| Line | Revision | Contents |
| 1 | 17 | /* Calliope Music Player |
| 2 | 185 | * Copyright 2005-09 Sam Thursfield <ssssam gmail.com> |
| 3 | 17 | * |
| 4 | * This program is free software: you can redistribute it and/or modify | |
| 5 | * it under the terms of the GNU General Public License as published by | |
| 6 | * the Free Software Foundation, either version 3 of the License, or | |
| 7 | * (at your option) any later version. | |
| 8 | * | |
| 9 | * This program is distributed in the hope that it will be useful, | |
| 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 12 | * GNU General Public License for more details. | |
| 13 | * | |
| 14 | * You should have received a copy of the GNU General Public License | |
| 15 | 264 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | 17 | */ |
| 17 | 264 | |
| 18 | // This is an abstract class which contains code for an in-memory database. | |
| 19 | 17 | // It is the basis for filesource and cdsource. |
| 20 | // | |
| 21 | ||
| 22 | #include <string.h> | |
| 23 | #include <stdlib.h> | |
| 24 | #include <glib.h> | |
| 25 | #include "main.h" | |
| 26 | #include "musicsourceimporting.h" | |
| 27 | 451 | #include "musicsource-private.h" |
| 28 | 24 | #include "musicsourceview.h" |
| 29 | 17 | |
| 30 | 445 | typedef struct { |
| 31 | Entry *entry; | |
| 32 | ||
| 33 | /* When in a notify function, removing entries fully can mess up the entry duplicates (because | |
| 34 | * it involves zeroing their foreign keys). In this case we remove the entries from the source | |
| 35 | * as soon as we can (to avoid them coming back from the dead in queries) but don't unref them | |
| 36 | * until we aren't notifying any more. */ | |
| 37 | gboolean removed_from_source; | |
| 38 | } RemovalQueueEntry; | |
| 39 | ||
| 40 | 17 | struct MusicSourceImportingPrivate { |
| 41 | 445 | /* Removing entries gets very messy; the global removal queue keeps things in order by ensuring |
| 42 | * we do things in the right order and don't confuse ourself with things like a removal notify | |
| 43 | * that causes removal of the entry it triggered on. */ | |
| 44 | GSList *removal_queue; | |
| 45 | 17 | }; |
| 46 | ||
| 47 | 264 | G_DEFINE_TYPE(MusicSourceImporting, music_source_importing, MUSIC_SOURCE_TYPE); |
| 48 | 17 | |
| 49 | static void finalize(GObject *self); | |
| 50 | ||
| 51 | static void init_db(MusicSourceImporting *self); | |
| 52 | static void free_db(MusicSourceImporting *self); | |
| 53 | ||
| 54 | 451 | static int find_entry (MusicSource *music_source, Entry *entry, Entry *caller); |
| 55 | ||
| 56 | 17 | static int is_empty(MusicSource *musicsource); |
| 57 | static int get_n_entries(MusicSource *musicsource, EntryType type, int *highest_id, int *n_dead); | |
| 58 | static gboolean is_valid_id(MusicSource *musicsource, EntryType type, int id); | |
| 59 | static Entry *query_entry(MusicSource *musicsource, EntryType type, int id); | |
| 60 | 102 | static int query_n_relations(MusicSource *music_source, int local_id, EntryType foreign_type, int foreign_property_id, int limit); |
| 61 | 17 | static GSList *query_entry_children(MusicSource *musicsource, int parent_id, EntryType child_entry_type, int child_property); |
| 62 | static GSList *query_entry_children_ids(MusicSource *musicsource, int parent_id, EntryType child_entry_type, int child_property); | |
| 63 | 92 | static GSList *query_relations(MusicSource *musicsource, EntryType local_type, int local_id, int relation_apid); |
| 64 | 17 | static GSList *query_ids(MusicSource *musicsource, EntryType entry_type); |
| 65 | static GSList *query_matching_except(MusicSource *musicsource, Entry *entry, int ignored_property_absolute_id); | |
| 66 | 24 | |
| 67 | 215 | static MusicSourceView *create_view(MusicSource *source, ViewConfig *config); |
| 68 | 24 | |
| 69 | 451 | static void add_entry_internal (MusicSource *music_source, Entry *entry); |
| 70 | static void remove_entry_internal (MusicSource *music_source, EntryType type, int id); | |
| 71 | static void update_foreign_keys (MusicSource *music_source, Entry *old_entry, Entry *new_entry); | |
| 72 | ||
| 73 | 17 | static int add_entry(MusicSource *musicsource, Entry *entry); |
| 74 | static void remove_entry(MusicSource *musicsource, EntryType type, int id); | |
| 75 | static Entry *checkout_entry(MusicSource *self, EntryType type, int id); | |
| 76 | static void checkin_entry(MusicSource *self, Entry *entry); | |
| 77 | 95 | static void flush(MusicSource *musicsource); |
| 78 | 17 | static void wipe(MusicSource *musicsource); |
| 79 | 206 | static void dump (MusicSource *musicsource, gboolean detailed); |
| 80 | 17 | |
| 81 | 269 | static void editing_trace (const char *format, ...); |
| 82 | 17 | |
| 83 | 445 | static gint removal_queue_compare_func (gconstpointer value, gconstpointer data) { |
| 84 | const RemovalQueueEntry *removal = value; | |
| 85 | if (removal->entry == data) | |
| 86 | return 0; | |
| 87 | return 1; | |
| 88 | } | |
| 89 | ||
| 90 | 17 | static void music_source_importing_class_init(MusicSourceImportingClass *cl) { |
| 91 | music_source_importing_parent_class=g_type_class_peek_parent(G_OBJECT_CLASS(cl)); | |
| 92 | G_OBJECT_CLASS(cl)->finalize=finalize; | |
| 93 | 451 | |
| 94 | MusicSourceClass *music_source_class = MUSIC_SOURCE_CLASS(cl); | |
| 95 | music_source_class->find_entry = find_entry; | |
| 96 | 17 | MUSIC_SOURCE_CLASS(cl)->is_empty=is_empty; |
| 97 | MUSIC_SOURCE_CLASS(cl)->get_n_entries=get_n_entries; | |
| 98 | MUSIC_SOURCE_CLASS(cl)->is_valid_id=is_valid_id; | |
| 99 | MUSIC_SOURCE_CLASS(cl)->query_entry=query_entry; | |
| 100 | 102 | MUSIC_SOURCE_CLASS(cl)->query_n_relations = query_n_relations; |
| 101 | 17 | MUSIC_SOURCE_CLASS(cl)->query_entry_children=query_entry_children; |
| 102 | MUSIC_SOURCE_CLASS(cl)->query_entry_children_ids=query_entry_children_ids; | |
| 103 | 92 | MUSIC_SOURCE_CLASS(cl)->query_relations = query_relations; |
| 104 | 17 | MUSIC_SOURCE_CLASS(cl)->query_ids=query_ids; |
| 105 | MUSIC_SOURCE_CLASS(cl)->query_matching_except=query_matching_except; | |
| 106 | 24 | MUSIC_SOURCE_CLASS(cl)->create_view=create_view; |
| 107 | 451 | music_source_class->add_entry_internal = add_entry_internal; |
| 108 | music_source_class->update_entry_internal = NULL; | |
| 109 | music_source_class->remove_entry_internal = remove_entry_internal; | |
| 110 | music_source_class->update_foreign_keys = update_foreign_keys; | |
| 111 | music_source_class->add_entry = add_entry; | |
| 112 | music_source_class->remove_entry = remove_entry; | |
| 113 | 17 | MUSIC_SOURCE_CLASS(cl)->checkout_entry=checkout_entry; |
| 114 | MUSIC_SOURCE_CLASS(cl)->checkin_entry=checkin_entry; | |
| 115 | 95 | MUSIC_SOURCE_CLASS(cl)->flush = flush; |
| 116 | 17 | MUSIC_SOURCE_CLASS(cl)->wipe=wipe; |
| 117 | 206 | MUSIC_SOURCE_CLASS(cl)->dump = dump; |
| 118 | g_type_class_add_private (cl, sizeof(MusicSourceImportingPrivate)); | |
| 119 | 17 | } |
| 120 | ||
| 121 | static void music_source_importing_init(MusicSourceImporting *self) { | |
| 122 | SP=G_TYPE_INSTANCE_GET_PRIVATE(self, MUSIC_SOURCE_IMPORTING_TYPE, MusicSourceImportingPrivate); | |
| 123 | ||
| 124 | init_db(self); | |
| 125 | 445 | |
| 126 | SP->removal_queue = NULL; | |
| 127 | 17 | } |
| 128 | ||
| 129 | static void finalize(GObject *object) { | |
| 130 | 264 | MusicSourceImporting *self=MUSIC_SOURCE_IMPORTING(object); |
| 131 | 17 | |
| 132 | free_db(self); | |
| 133 | ||
| 134 | 445 | if (SP->removal_queue != NULL) |
| 135 | g_warning ("genericsource: removal queue not empty at finalize: %i entries.\n", | |
| 136 | g_slist_length (SP->removal_queue)); | |
| 137 | ||
| 138 | 17 | G_OBJECT_CLASS(music_source_importing_parent_class)->finalize(object); |
| 139 | }; | |
| 140 | ||
| 141 | static void init_db(MusicSourceImporting *self) { | |
| 142 | 271 | for (int i=0; i<ENTRY_TYPE_COUNT; i++) { |
| 143 | 17 | self->data[i]=g_ptr_array_new(); |
| 144 | self->total[i]=0; self->dead[i]=0; | |
| 145 | }; | |
| 146 | 264 | |
| 147 | 17 | // FIXME: !! |
| 148 | // FIXME: how can we make these consistently sort last ? What | |
| 149 | // if there is a band called like ZZZZ Top ?? | |
| 150 | self->unknown_artist_entry=entry_new(ENTRY_TYPE_ARTIST, ARTIST_ID_UNKNOWN, self, "musicsourceimporting database"); | |
| 151 | entry_set_property(self->unknown_artist_entry, ARTIST_NAME, "Unknown Artist"); | |
| 152 | entry_set_property(self->unknown_artist_entry, ARTIST_SORTNAME, "ZZZZZ"); | |
| 153 | ||
| 154 | self->various_artists_entry=entry_new(ENTRY_TYPE_ARTIST, ARTIST_ID_VARIOUS, self, "musicsourceimporting database"); | |
| 155 | entry_set_property(self->various_artists_entry, ARTIST_NAME, "Various Artists"); | |
| 156 | entry_set_property(self->various_artists_entry, ARTIST_SORTNAME, "ZZZZY"); | |
| 157 | ||
| 158 | self->total[ENTRY_TYPE_ARTIST]=2; | |
| 159 | 264 | g_ptr_array_add(self->data[ENTRY_TYPE_ARTIST], self->unknown_artist_entry); |
| 160 | 17 | g_ptr_array_add(self->data[ENTRY_TYPE_ARTIST], self->various_artists_entry); |
| 161 | }; | |
| 162 | ||
| 163 | static void free_db(MusicSourceImporting *self) { | |
| 164 | 273 | // First we NULL all of the foreign keys in the database, because otherwise the entries are all |
| 165 | // still referencing each other and nothing would really happen. | |
| 166 | 283 | // FIXME: shouldn't have to .. |
| 167 | 273 | for (int i=0; i<ENTRY_TYPE_COUNT; i++) { |
| 168 | for (int j=0; j<self->data[i]->len; j++) { | |
| 169 | Entry *entry = g_ptr_array_index(self->data[i], j); | |
| 170 | if (entry==NULL) continue; | |
| 171 | ||
| 172 | for (int p=0; p<entry_n_properties[i]; p++) { | |
| 173 | if (IS_FOREIGN_KEY(schema[i][p].type)) | |
| 174 | entry_set_property (entry, p, NULL); | |
| 175 | }; | |
| 176 | }; | |
| 177 | }; | |
| 178 | ||
| 179 | for (int i=0; i<ENTRY_TYPE_COUNT; i++) { | |
| 180 | int n_dead = 0; | |
| 181 | for (int j=0; j<self->data[i]->len; j++) { | |
| 182 | Entry *entry = g_ptr_array_index(self->data[i], j); | |
| 183 | if (entry!=NULL) | |
| 184 | entry_unref (entry, "musicsourceimporting database"); | |
| 185 | else n_dead++; | |
| 186 | }; | |
| 187 | ||
| 188 | g_warn_if_fail (self->dead[i] == n_dead); | |
| 189 | g_ptr_array_free (self->data[i], TRUE); | |
| 190 | 17 | } |
| 191 | }; | |
| 192 | ||
| 193 | 451 | /************************************************************************************************* |
| 194 | * Querying - internal functions. | |
| 195 | */ | |
| 196 | ||
| 197 | /* FIXME: make this much faster! In importing its only caller is try_merge, by the way */ | |
| 198 | static int find_entry (MusicSource *music_source, Entry *entry, Entry *caller) { | |
| 199 | MusicSourceImporting *self = MUSIC_SOURCE_IMPORTING(music_source); | |
| 200 | for (int i=0; i<self->total[entry->type]; i++) { | |
| 201 | Entry *matching_entry = (Entry *)g_ptr_array_index(self->data[entry->type], i); | |
| 202 | ||
| 203 | if (matching_entry==NULL) | |
| 204 | continue; | |
| 205 | ||
| 206 | if (entry==matching_entry) | |
| 207 | return i + 1; | |
| 208 | ||
| 209 | if (entry_match (entry, matching_entry)) { | |
| 210 | return i + 1; | |
| 211 | } | |
| 212 | }; | |
| 213 | ||
| 214 | return 0; | |
| 215 | } | |
| 216 | ||
| 217 | ||
| 218 | /************************************************************************************************* | |
| 219 | * Querying - MusicSource interface | |
| 220 | */ | |
| 221 | ||
| 222 | 17 | static gboolean is_empty(MusicSource *musicsource) { |
| 223 | 264 | // FIXME: we should could dead recordings I think. Where is this function even used ?? |
| 224 | if (MUSIC_SOURCE_IMPORTING(musicsource)->total[ENTRY_TYPE_RECORDING]>0) | |
| 225 | 17 | return FALSE; |
| 226 | 264 | if (!music_source_get_loaded(musicsource)) |
| 227 | 17 | return FALSE; |
| 228 | return TRUE; | |
| 229 | }; | |
| 230 | ||
| 231 | static int get_n_entries(MusicSource *musicsource, EntryType type, int *highest_id, int *n_dead) { | |
| 232 | 271 | MusicSourceImporting *self = MUSIC_SOURCE_IMPORTING(musicsource); |
| 233 | if (highest_id!=NULL) *highest_id = self->total[type]; | |
| 234 | if (n_dead!=NULL) *n_dead = self->dead[type]; | |
| 235 | return self->total[type] - self->dead[type]; | |
| 236 | 17 | }; |
| 237 | ||
| 238 | static gboolean is_valid_id(MusicSource *musicsource, EntryType type, int id) { | |
| 239 | MusicSourceImporting *self=MUSIC_SOURCE_IMPORTING(musicsource); | |
| 240 | 271 | if (id > self->total[type]) return FALSE; |
| 241 | 17 | if (g_ptr_array_index(self->data[type], id-1)==NULL) return FALSE; |
| 242 | return TRUE; | |
| 243 | 264 | }; |
| 244 | 17 | |
| 245 | static Entry *query_entry(MusicSource *musicsource, EntryType type, int id) { | |
| 246 | //printf("music-source-importing::query-entry %i %i\n", type, id);fflush(stdout); | |
| 247 | MusicSourceImporting *self=MUSIC_SOURCE_IMPORTING(musicsource); | |
| 248 | 91 | g_return_val_if_fail (id>0 && id<=self->total[type], NULL); |
| 249 | 24 | |
| 250 | 17 | Entry *entry=g_ptr_array_index(self->data[type], id-1); |
| 251 | 264 | if (entry!=NULL) |
| 252 | 17 | entry_ref(entry, "musicsourceimporting::query-entry"); |
| 253 | return entry; | |
| 254 | 264 | }; |
| 255 | 17 | |
| 256 | 102 | // Used mainly to find if an entry at a m:1 join is orphaned or has only one parent. |
| 257 | // FIXME: clearly a slow way of doing things ! | |
| 258 | 264 | int query_n_relations (MusicSource *musicsource, int local_id, EntryType foreign_type, |
| 259 | 209 | int foreign_property_id, int limit) { |
| 260 | 102 | int count = 0; |
| 261 | 264 | |
| 262 | 102 | MusicSourceImporting *self = MUSIC_SOURCE_IMPORTING(musicsource); |
| 263 | for (int i=0; i<self->total[foreign_type]; i++) { | |
| 264 | Entry *foreign_entry = g_ptr_array_index(self->data[foreign_type], i); | |
| 265 | if (foreign_entry!=NULL) { | |
| 266 | Entry *child_entry = entry_get_property(foreign_entry, foreign_property_id); | |
| 267 | 264 | //printf ("%s %i %s: %i.\n", ENTRY_PF(foreign_entry), |
| 268 | // schema[foreign_type][foreign_property_id].name, child_entry->id); | |
| 269 | 235 | fflush (stdout); |
| 270 | 102 | if (child_entry->id==local_id) { |
| 271 | 227 | //printf ("%s %i is related.\n", ENTRY_PF(child_entry)); fflush (stdout); |
| 272 | 102 | count++; |
| 273 | 264 | if (count>=limit) |
| 274 | 102 | break; |
| 275 | }; | |
| 276 | }; | |
| 277 | }; | |
| 278 | return count; | |
| 279 | }; | |
| 280 | ||
| 281 | 17 | static GSList *query_entry_children(MusicSource *musicsource, int parent_id, EntryType child_entry_type, int child_property) { |
| 282 | MusicSourceImporting *self=MUSIC_SOURCE_IMPORTING(musicsource); | |
| 283 | GSList *list=NULL; | |
| 284 | 264 | |
| 285 | 17 | for (int i=0;i<self->total[child_entry_type];i++) { |
| 286 | Entry *child_entry=g_ptr_array_index(self->data[child_entry_type], i); | |
| 287 | if (child_entry!=NULL) { | |
| 288 | Entry *child_parent_entry=entry_get_property(child_entry, child_property); | |
| 289 | if (child_parent_entry!=NULL && parent_id==child_parent_entry->id) { | |
| 290 | 264 | entry_ref(child_entry, "musicsourceimporting::query-entry-children"); |
| 291 | 17 | list=g_slist_prepend(list, child_entry); |
| 292 | }; | |
| 293 | } | |
| 294 | 264 | }; |
| 295 | 17 | return list; |
| 296 | 264 | }; |
| 297 | 17 | |
| 298 | static GSList *query_entry_children_ids(MusicSource *musicsource, int parent_id, EntryType child_entry_type, int child_property) { | |
| 299 | //printf("hello!\n");fflush(stdout); | |
| 300 | MusicSourceImporting *self=MUSIC_SOURCE_IMPORTING(musicsource); | |
| 301 | GSList *list=NULL; | |
| 302 | 264 | |
| 303 | 17 | for (int i=0;i<self->total[child_entry_type];i++) { |
| 304 | 92 | Entry *child_entry = g_ptr_array_index(self->data[child_entry_type], i); |
| 305 | 17 | if (child_entry!=NULL) { |
| 306 | 92 | Entry *child_parent_entry = entry_get_property(child_entry, child_property); |
| 307 | 17 | if (child_parent_entry!=NULL && parent_id==child_parent_entry->id) |
| 308 | 92 | list = g_slist_prepend(list, (void *)child_entry->id); |
| 309 | 17 | } |
| 310 | 264 | }; |
| 311 | 17 | return list; |
| 312 | 264 | }; |
| 313 | 17 | |
| 314 | 92 | // FIXME: this function should subsume query_entry_children |
| 315 | 264 | static GSList *query_relations(MusicSource *musicsource, EntryType local_type, int local_id, |
| 316 | 92 | int relation_apid) { |
| 317 | 264 | g_return_val_if_fail (relation_apid!=-1, NULL); |
| 318 | ||
| 319 | 92 | MusicSourceImporting *self=MUSIC_SOURCE_IMPORTING(musicsource); |
| 320 | GSList *list=NULL; | |
| 321 | ||
| 322 | if (APID_GET_TYPE(relation_apid)==local_type) { | |
| 323 | // All entries local refers to in a property | |
| 324 | 264 | return query_entry_children (musicsource, local_id, |
| 325 | 92 | APID_GET_PROPERTY(relation_apid).type, |
| 326 | APID_GET_PROPERTY_ID(relation_apid)); | |
| 327 | } else if (APID_GET_PROPERTY(relation_apid).type==local_type) { | |
| 328 | // All entries that refer to local in a property | |
| 329 | for (int i=0; i<self->total[APID_GET_TYPE(relation_apid)]; i++) { | |
| 330 | Entry *foreign = g_ptr_array_index(self->data[APID_GET_TYPE(relation_apid)], i); | |
| 331 | if (foreign!=NULL) { | |
| 332 | Entry *target = entry_get_property(foreign, APID_GET_PROPERTY_ID(relation_apid)); | |
| 333 | g_warn_if_fail (target->type==local_type); | |
| 334 | if (target!=NULL && target->id==local_id) { | |
| 335 | entry_ref (foreign, "generic-source::query-relations"); | |
| 336 | list = g_slist_prepend(list, (void *)foreign); | |
| 337 | }; | |
| 338 | } | |
| 339 | 264 | }; |
| 340 | return list; | |
| 341 | 111 | } else { |
| 342 | 104 | g_warning ("query-relations: %s[%i] on %s.%s :(", entry_type_name[local_type], local_id, |
| 343 | APID_NAME(relation_apid)); | |
| 344 | 111 | return NULL; |
| 345 | }; | |
| 346 | 92 | }; |
| 347 | ||
| 348 | 17 | static GSList *query_ids(MusicSource *musicsource, EntryType entry_type) { |
| 349 | MusicSourceImporting *self=MUSIC_SOURCE_IMPORTING(musicsource); | |
| 350 | GSList *list=NULL; | |
| 351 | 264 | |
| 352 | 17 | for (int id=1;id<=self->total[entry_type];id++) { |
| 353 | Entry *entry=g_ptr_array_index(self->data[entry_type], id-1); | |
| 354 | if (entry!=NULL) { | |
| 355 | list=g_slist_prepend(list, GINT_TO_POINTER(entry->id)); | |
| 356 | }; | |
| 357 | }; | |
| 358 | ||
| 359 | return list; | |
| 360 | }; | |
| 361 | ||
| 362 | GSList *query_matching_except(MusicSource *musicsource, Entry *entry, int ignored_property_absolute_id) { | |
| 363 | // FIXME: this needs to be really fast, because it is called on | |
| 364 | // releases for every single track add !! | |
| 365 | MusicSourceImporting *self=MUSIC_SOURCE_IMPORTING(musicsource); | |
| 366 | GSList *result=NULL; | |
| 367 | 264 | |
| 368 | 17 | for (int i=0;i<self->total[entry->type];i++) { |
| 369 | Entry *current_entry=g_ptr_array_index(self->data[entry->type], i); | |
| 370 | 264 | if (current_entry!=NULL && current_entry!=entry && |
| 371 | 17 | entry_match_except(entry, current_entry, ignored_property_absolute_id)) { |
| 372 | entry_ref(current_entry, "musicsourceimporting::query-matching-except"); | |
| 373 | 264 | result=g_slist_prepend(result, current_entry); |
| 374 | 17 | }; |
| 375 | }; | |
| 376 | ||
| 377 | return result; | |
| 378 | }; | |
| 379 | ||
| 380 | 24 | |
| 381 | ||
| 382 | 215 | static MusicSourceView *create_view(MusicSource *source, ViewConfig *config) { |
| 383 | 51 | return g_object_new(GENERIC_VIEW_TYPE, "source", source, "config", config, NULL); |
| 384 | 24 | }; |
| 385 | ||
| 386 | 451 | |
| 387 | /************************************************************************************************* | |
| 388 | * Editing - internal functions. | |
| 389 | * | |
| 390 | * FIXME: the functions here and in Library have the same behaviour, so maybe they should become | |
| 391 | * private superclass methods. There's no technical reason to do that though, so the question | |
| 392 | * is would it make the code any clearer? | |
| 393 | */ | |
| 394 | ||
| 395 | static void add_entry_internal (MusicSource *music_source, Entry *entry) { | |
| 396 | MusicSourceImporting *self = MUSIC_SOURCE_IMPORTING(music_source); | |
| 397 | 271 | entry_ref_invisible (entry, "musicsourceimporting database"); |
| 398 | ||
| 399 | if (self->dead[entry->type] > 0) { | |
| 400 | 17 | // Fill in an unused ID if possible |
| 401 | 271 | for (int i=0; i<self->total[entry->type]; i++) { |
| 402 | 17 | if (g_ptr_array_index(self->data[entry->type], i)==NULL) { |
| 403 | 275 | _entry_set_id (entry, i+1); |
| 404 | 271 | g_ptr_array_index(self->data[entry->type], i) = entry; |
| 405 | 17 | self->dead[entry->type]--; |
| 406 | break; | |
| 407 | }; | |
| 408 | }; | |
| 409 | } else { | |
| 410 | 271 | int id = ++self->total[entry->type]; |
| 411 | 275 | _entry_set_id (entry, id); |
| 412 | 271 | g_ptr_array_add (self->data[entry->type], entry); |
| 413 | 17 | }; |
| 414 | 271 | entry->source = self; |
| 415 | 17 | }; |
| 416 | ||
| 417 | 451 | static void remove_entry_internal (MusicSource *music_source, EntryType type, int id) { |
| 418 | MusicSourceImporting *self = MUSIC_SOURCE_IMPORTING(music_source); | |
| 419 | 445 | editing_trace ("remove_entry_internal: %s %i\n", entry_type_name[type], id); |
| 420 | 271 | |
| 421 | Entry *dead_entry = g_ptr_array_index(self->data[type], id-1); | |
| 422 | g_return_if_fail (dead_entry!=NULL); | |
| 423 | g_return_if_fail (dead_entry->type==type && dead_entry->id==id); | |
| 424 | 415 | |
| 425 | 271 | entry_unref (dead_entry, "musicsourceimporting database"); |
| 426 | g_ptr_array_index(self->data[type], id-1) = NULL; | |
| 427 | 17 | self->dead[type]++; |
| 428 | 271 | |
| 429 | g_warn_if_fail (self->total[type]-self->dead[type] >= 0); | |
| 430 | 17 | }; |
| 431 | ||
| 432 | 451 | /* update_foreign_keys: used when entries are merged, to update properties that refer to the |
| 433 | * removed one 'old_entry' and should point to the kept one 'new_entry' instead | |
| 434 | */ | |
| 435 | static void update_foreign_keys (MusicSource *music_source, Entry *from_entry, Entry *to_entry) { | |
| 436 | MusicSourceImporting *self = MUSIC_SOURCE_IMPORTING(music_source); | |
| 437 | ||
| 438 | for (int i=0; i<ENTRY_TYPE_COUNT; i++) { | |
| 439 | for (int p=0; p<entry_n_properties[i]; p++) { | |
| 440 | if (schema[i][p].type==from_entry->type) { | |
| 441 | for (int k=0; k<self->total[i]; k++) { | |
| 442 | Entry *linked_entry = g_ptr_array_index(self->data[i], k); | |
| 443 | ||
| 444 | if (linked_entry!=NULL) { | |
| 445 | if ((Entry *)entry_get_property(linked_entry, p)==from_entry) { | |
| 446 | entry_set_property (linked_entry, p, to_entry); | |
| 447 | ||
| 448 | // For each checked-out entry that references 'from_entry', we need to | |
| 449 | // unref the checkout copies of 'from_entry' now. | |
| 450 | // FIXME: make the checkout store a btree or something, so this is | |
| 451 | // actually practical. | |
| 452 | // FIXME: is it correct that we only unref once even if there are | |
| 453 | // multiple duplicate versions? | |
| 454 | if (_music_source_entry_is_checked_out | |
| 455 | (MUSIC_SOURCE(self), linked_entry->type, linked_entry->id)) { | |
| 456 | editing_trace("Found. Unref dup of %s %i\n", ENTRY_PF(from_entry)); | |
| 457 | _music_source_unref_checkout_copies (MUSIC_SOURCE(self), | |
| 458 | from_entry->type, | |
| 459 | from_entry->id); | |
| 460 | } | |
| 461 | ||
| 462 | #ifdef ENTRY_TRACK_REFERENCE_OWNERS | |
| 463 | // Update these too ! | |
| 464 | for (GList *node=linked_entry->reference_owner_list; node!=NULL; | |
| 465 | node=node->next) { | |
| 466 | EntryReferenceOwner *owner = node->data; | |
| 467 | if (owner->name==from_entry->id_string) { | |
| 468 | owner->name = to_entry->id_string; | |
| 469 | break; | |
| 470 | }; | |
| 471 | }; | |
| 472 | #endif | |
| 473 | }; | |
| 474 | } | |
| 475 | }; | |
| 476 | 17 | }; |
| 477 | }; | |
| 478 | 451 | }; |
| 479 | } | |
| 480 | ||
| 481 | ||
| 482 | 17 | |
| 483 | ||
| 484 | 415 | /* count_references: internal routine used by 'touch'. Checks if entry is not reffed by anything, |
| 485 | 445 | * taking into account the source's removal queue. |
| 486 | 415 | * |
| 487 | * FIXME: we could use the method in library instead, which would be a lot slower .. | |
| 488 | 451 | * Or at least, we could check the refcount is only a source and the entries when entry |
| 489 | * ref tracking is enabled, so know that this method will work at least | |
| 490 | */ | |
| 491 | 445 | static int count_references (MusicSourceImporting *self, Entry *entry, EntryType caller_type, |
| 492 | int caller_id) { | |
| 493 | 415 | // We allow one ref for the source. |
| 494 | int ref_count = entry->ref_count - 1; | |
| 495 | ||
| 496 | // Find anything in the removal queue with the type being counted, and ignore the | |
| 497 | // references those entries contain. | |
| 498 | // | |
| 499 | GSList *node = entry_type_info[entry->type].foreign_key_owners; | |
| 500 | while(node!=NULL) { | |
| 501 | const int apid = (int)node->data; | |
| 502 | 445 | if (SP->removal_queue!=NULL) |
| 503 | for (GSList *q_node=SP->removal_queue; q_node; q_node=q_node->next) { | |
| 504 | RemovalQueueEntry *removal = q_node->data; | |
| 505 | Entry *q_entry = removal->entry; | |
| 506 | 415 | if (q_entry->type==APID_GET_TYPE(apid) |
| 507 | && entry_get_property(entry, APID_GET_PROPERTY_ID(apid))==entry) | |
| 508 | ref_count --; | |
| 509 | }; | |
| 510 | ||
| 511 | node = node->next; | |
| 512 | }; | |
| 513 | ||
| 514 | 451 | // Because checkout duplicates point to real database entries, we need to ignore their |
| 515 | // references. | |
| 516 | 445 | ref_count -= _music_source_count_checkout_duplicate_references (MUSIC_SOURCE(self), entry); |
| 517 | ||
| 518 | editing_trace ("Found %i refs for %s %i \n", ref_count, ENTRY_PF(entry)); | |
| 519 | 452 | //printf ("genericsource: Count_references: %s %i = %i\n", ENTRY_PF(entry), ref_count); fflush (stdout); |
| 520 | 415 | return ref_count; |
| 521 | }; | |
| 522 | ||
| 523 | 451 | /* touch: garbage collection. */ |
| 524 | static void touch (MusicSourceImporting *self, Entry *entry, gboolean delete, | |
| 525 | gboolean will_be_unreffed, GSList **p_jetsam, EntryType caller_type, | |
| 526 | int caller_id) { | |
| 527 | g_return_if_fail (entry != NULL); | |
| 528 | 269 | const EntryType type = entry->type; const int id = entry->id; |
| 529 | 451 | |
| 530 | if (ENTRY_IS_SPECIAL(type, id)) | |
| 531 | return; | |
| 532 | 415 | |
| 533 | 358 | editing_trace ("touch: %s %i refs %i - d%i u%i.\n", entry_type_name[type], id, entry->ref_count, |
| 534 | 346 | delete, will_be_unreffed); |
| 535 | 269 | |
| 536 | if (delete) { | |
| 537 | // Search for types that point to this one (unless the link is flagged 'trivial') and | |
| 538 | 451 | // delete those. For example, if removing a recording this deletes all of its files. |
| 539 | 269 | // |
| 540 | for (EntryType i=0; i<ENTRY_TYPE_COUNT; i++) { | |
| 541 | // Entries linking to their own type is not allowed in Calliope anywhere | |
| 542 | // FIXME: although you don't mention that anywhere. | |
| 543 | if (i==type) continue; | |
| 544 | ||
| 545 | for (int p=0; p<entry_n_properties[i]; p++) { | |
| 546 | if (schema[i][p].type!=type) continue; | |
| 547 | ||
| 548 | 415 | // Look through all our entries of type 'i' for those linking to 'entry' |
| 549 | for (int k=0; k < self->total[i]; k++) { | |
| 550 | 269 | Entry *child_entry = g_ptr_array_index(self->data[i], k); |
| 551 | 415 | |
| 552 | 269 | // Ignore dead entries, and the entry that triggered this update (to prevent |
| 553 | // traversing back down the tree again and making an infinite loop). | |
| 554 | if (child_entry==NULL || (i==caller_type && child_entry->id==caller_id)) | |
| 555 | continue; | |
| 556 | ||
| 557 | if ((Entry *)entry_get_property(child_entry, p)!=entry) continue; | |
| 558 | ||
| 559 | 451 | // If the foreign key is flagged trivial, just set the value to NULL if it |
| 560 | // references this entry instead of deleting it. | |
| 561 | const gboolean delete_child = !(schema[i][p].flags & PROPERTY_TRIVIAL); | |
| 562 | touch (self, child_entry, delete_child, FALSE, p_jetsam, type, id); | |
| 563 | 269 | }; |
| 564 | }; | |
| 565 | }; | |
| 566 | }; | |
| 567 | ||
| 568 | if (!delete) { | |
| 569 | 445 | int ref_count = count_references(self, entry, caller_type, caller_id); |
| 570 | 451 | |
| 571 | // It is valid for an entry with no references to be touched with will_be_unreffed TRUE - | |
| 572 | // all the entries that point to it could be in the removal queue, even though they can't | |
| 573 | // yet have been removed. | |
| 574 | 415 | g_warn_if_fail (ref_count >= 0); |
| 575 | if (will_be_unreffed) ref_count--; | |
| 576 | 451 | |
| 577 | 415 | delete = (ref_count <= 0); |
| 578 | 269 | }; |
| 579 | ||
| 580 | 344 | // Touch all entries this one links to, to see if they now need removing or 'changed' emitting. |
| 581 | 269 | for (int p=0; p<entry_n_properties[type]; p++) { |
| 582 | if (IS_FOREIGN_KEY(schema[type][p].type)) { | |
| 583 | // Parent is eg a recording's composition | |
| 584 | Entry *parent = entry_get_property(entry, p); | |
| 585 | if (parent!=NULL) { | |
| 586 | if (parent->type!=caller_type || parent->id!=caller_id) | |
| 587 | 445 | touch (self, parent, FALSE, delete, p_jetsam, type, id); |
| 588 | 269 | }; |
| 589 | }; | |
| 590 | }; | |
| 591 | ||
| 592 | if (delete) { | |
| 593 | 445 | if (!g_slist_find_custom (SP->removal_queue, entry, removal_queue_compare_func)) { |
| 594 | RemovalQueueEntry *removal = g_slice_new0 (RemovalQueueEntry); | |
| 595 | removal->entry = entry; | |
| 596 | SP->removal_queue = g_slist_prepend (SP->removal_queue, removal); | |
| 597 | _music_source_queue_notify (MUSIC_SOURCE(self), entry, NULL); | |
| 598 | } | |
| 599 | 318 | |
| 600 | 415 | // Also remove from jetsam. |
| 601 | if (p_jetsam != NULL) | |
| 602 | for (GSList *node=*p_jetsam; node; node=node->next) { | |
| 603 | Entry *jetsam_entry = node->data; | |
| 604 | if (jetsam_entry->type==type && jetsam_entry->id==id) { | |
| 605 | (*p_jetsam) = g_slist_remove_link (*p_jetsam, node); | |
| 606 | break; | |
| 607 | 318 | }; |
| 608 | 415 | }; |
| 609 | }; | |
| 610 | 269 | } |
| 611 | ||
| 612 | ||
| 613 | 445 | static void execute_removal_queue (MusicSourceImporting *self) { |
| 614 | 451 | MusicSource *music_source = MUSIC_SOURCE(self); |
| 615 | 445 | editing_trace ("EXECUTE_REMOVAL_QUEUE! (%i entries) %i\n\n", g_slist_length (SP->removal_queue), |
| 616 | 451 | _music_source_inside_notify (music_source)); |
| 617 | 445 | |
| 618 | // When we are still inside a notify function, we don't want to mess up entries that might still | |
| 619 | // be referenced by checkout duplicates. We remove them from the source but keep them in the | |
| 620 | // queue, so they are accessible but won't be returned in future queries. | |
| 621 | 451 | const gboolean in_notify = _music_source_inside_notify (music_source); |
| 622 | 415 | |
| 623 | 445 | if (!in_notify) { |
| 624 | // First zero all of their foreign keys, to prevent circular referencing keeping entries | |
| 625 | // alive. | |
| 626 | for (GSList *q_node=SP->removal_queue; q_node; q_node=q_node->next) { | |
| 627 | RemovalQueueEntry *removal = q_node->data; | |
| 628 | Entry *removal_entry = removal->entry; | |
| 629 | 451 | entry_destroy (removal_entry); |
| 630 | } | |
| 631 | 445 | } |
| 632 | ||
| 633 | // Now actually delete the queued entries from the source. Any that are used in duplicates will | |
| 634 | // be kept alive by references from the duplicates. | |
| 635 | for (GSList *q_node=SP->removal_queue; q_node; q_node=q_node->next) { | |
| 636 | RemovalQueueEntry *removal = q_node->data; | |
| 637 | Entry *removal_entry = removal->entry; | |
| 638 | ||
| 639 | editing_trace ("\t%s %i: %i.\n", ENTRY_PF(removal_entry), removal->removed_from_source); | |
| 640 | if (in_notify) | |
| 641 | entry_ref (removal->entry, "genericsource:kept alive during notify"); | |
| 642 | ||
| 643 | if (!removal->removed_from_source) | |
| 644 | 451 | remove_entry_internal (music_source, removal_entry->type, removal_entry->id); |
| 645 | 445 | else |
| 646 | entry_unref (removal->entry, "genericsource:kept alive during notify"); | |
| 647 | ||
| 648 | 451 | if (_music_source_inside_notify (music_source)) |
| 649 | 445 | // Leave the entry in the queue, so we can zero its foreign keys later on. |
| 650 | removal->removed_from_source = TRUE; | |
| 651 | else | |
| 652 | g_slice_free (RemovalQueueEntry, removal); | |
| 653 | } | |
| 654 | ||
| 655 | if (!in_notify) { | |
| 656 | g_slist_free (SP->removal_queue); | |
| 657 | SP->removal_queue = NULL; | |
| 658 | } | |
| 659 | 415 | }; |
| 660 | ||
| 661 | 17 | // FIXME: this has to be slow with all the merging and shit. |
| 662 | 451 | static int add_entry (MusicSource *music_source, Entry *entry) { |
| 663 | entry_take_last_ref (entry, "musicsourceimporting::add_entry"); | |
| 664 | Entry *result = _music_source_add_entry_recursive (music_source, entry, TRUE, entry); | |
| 665 | int result_id = result->id; | |
| 666 | 264 | |
| 667 | 451 | entry_unref(entry, "musicsourceimporting::add_entry"); |
| 668 | 264 | if (result!=entry) |
| 669 | 451 | entry_unref(result, "_music_source_add_entry_recursive"); |
| 670 | 264 | |
| 671 | 451 | _music_source_emit_queued_notifications (music_source, NULL); |
| 672 | 452 | // FIXME: in library here we have to emit the removal queue, or tests like Album Notify 1 fail. |
| 673 | // I don't know why we don't need to in genericsource. | |
| 674 | 17 | return result_id; |
| 675 | }; | |
| 676 | ||
| 677 | 243 | static void remove_entry (MusicSource *musicsource, EntryType type, int id) { |
| 678 | MusicSourceImporting *self = MUSIC_SOURCE_IMPORTING(musicsource); | |
| 679 | 415 | g_return_if_fail (id > 0 && id <= self->total[type]); |
| 680 | 451 | |
| 681 | 243 | MUSIC_SOURCE_CLASS(music_source_importing_parent_class)->remove_entry (musicsource, type, id); |
| 682 | 264 | |
| 683 | 445 | g_warn_if_fail (SP->removal_queue == NULL); |
| 684 | 415 | |
| 685 | // Touch 'entry' with delete = TRUE | |
| 686 | 104 | Entry *entry = g_ptr_array_index(self->data[type], id-1); |
| 687 | 445 | touch (self, entry, TRUE, FALSE, NULL, 0, 0); |
| 688 | ||
| 689 | 364 | _music_source_emit_queued_notifications (musicsource, NULL); |
| 690 | 445 | execute_removal_queue (self); |
| 691 | 17 | }; |
| 692 | ||
| 693 | // FIXME: surely an entry can only be checked out once at a time? I don't know, | |
| 694 | // you should enforce this ! | |
| 695 | 451 | static Entry *checkout_entry (MusicSource *music_source, EntryType root_type, int root_id) { |
| 696 | MusicSourceImporting *self = MUSIC_SOURCE_IMPORTING(music_source); | |
| 697 | 264 | |
| 698 | 339 | Entry *entry = g_ptr_array_index(self->data[root_type], root_id-1); |
| 699 | g_return_val_if_fail (entry!=NULL, NULL); | |
| 700 | 342 | |
| 701 | 451 | _music_source_checkout_entry_recursive (music_source, entry); |
| 702 | 342 | |
| 703 | 339 | if (entry!=NULL) |
| 704 | entry_ref (entry, "musicsourceimporting::checkout-entry"); | |
| 705 | return entry; | |
| 706 | 17 | }; |
| 707 | ||
| 708 | 336 | /* checkin_entry: 'entry' has been changed, and our internal represenation needs updating. */ |
| 709 | 451 | static void checkin_entry (MusicSource *music_source, Entry *entry) { |
| 710 | 445 | MusicSourceImporting *self = MUSIC_SOURCE_IMPORTING(music_source); |
| 711 | 264 | |
| 712 | 445 | GSList *jetsam = NULL; |
| 713 | 451 | Entry *result = _music_source_checkin_entry_recursive (music_source, entry, &jetsam, entry); |
| 714 | 264 | |
| 715 | 17 | // Original entry is unreffed in music_source_checkin_entry |
| 716 | 451 | if (entry != result) |
| 717 | entry_unref (result, "_music_source_checkin_entry_recursive"); | |
| 718 | 348 | |
| 719 | 445 | // Touch each jetsam entry (something which was in the tree of entries checked out, but has now |
| 720 | // been replaced by a different entry). They will be removed if they are not referenced anywhere | |
| 721 | 345 | for (GSList *node=jetsam; node; node=node->next) |
| 722 | 445 | touch (self, node->data, FALSE, FALSE, &node->next, 0, 0); |
| 723 | 269 | g_slist_free (jetsam); |
| 724 | 418 | |
| 725 | 445 | _music_source_emit_queued_notifications (music_source, NULL); |
| 726 | execute_removal_queue (self); | |
| 727 | 17 | }; |
| 728 | ||
| 729 | 271 | |
| 730 | 95 | static void flush(MusicSource *musicsource) { |
| 731 | // No flushing to do here since we don't have a cache. | |
| 732 | }; | |
| 733 | ||
| 734 | 17 | static void wipe(MusicSource *musicsource) { |
| 735 | 264 | MusicSourceImporting *self=MUSIC_SOURCE_IMPORTING(musicsource); |
| 736 | 346 | _music_source_emit(musicsource, MUSIC_SOURCE_SIGNAL_WIPED, 0); |
| 737 | 17 | free_db(self); |
| 738 | 264 | init_db(self); |
| 739 | 17 | }; |
| 740 | ||
| 741 | // For debugging | |
| 742 | // | |
| 743 | 206 | static void dump (MusicSource *music_source, gboolean detailed) { |
| 744 | 264 | MusicSourceImporting *self = MUSIC_SOURCE_IMPORTING(music_source); |
| 745 | 206 | for (int i=0; i<ENTRY_TYPE_COUNT; i++) { |
| 746 | if (!detailed) { | |
| 747 | 264 | |
| 748 | 206 | int highest_id, n_dead, total = music_source_get_n_entries(music_source, i, &highest_id, |
| 749 | &n_dead); | |
| 750 | 264 | printf ("%s: total %i, highest %i with %i dead.\n", entry_type_name[i], total, |
| 751 | 206 | highest_id, n_dead); |
| 752 | } else { | |
| 753 | printf ("\n%s\n---------------\n", entry_type_name[i]); | |
| 754 | for (int j=0; j<self->total[i]; j++) { | |
| 755 | entry_dump ((Entry *)g_ptr_array_index(self->data[i], j)); | |
| 756 | }; | |
| 757 | 17 | }; |
| 758 | }; | |
| 759 | 206 | fflush (stdout); |
| 760 | 17 | }; |
| 761 | 269 | |
| 762 | ||
| 763 | static void editing_trace (const char *format, ...) { | |
| 764 | #ifdef MUSIC_SOURCE_TRACE_EDITING | |
| 765 | va_list va; va_start (va, format); | |
| 766 | 342 | //vprintf (format, va); |
| 767 | 337 | g_logv (NULL, G_LOG_LEVEL_DEBUG, format, va); |
| 768 | va_end (va); //fflush (stdout); | |
| 769 | 269 | #endif |
| 770 | }; |
Loggerhead is a web-based interface for Bazaar branches