1/*
2* Copyright (c) 2010-2013 Yorba Foundation
3*
4* This program is free software; you can redistribute it and/or
5* modify it under the terms of the GNU Lesser General Public
6* License as published by the Free Software Foundation; either
7* version 2.1 of the License, or (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 GNU
12* General Public License for more details.
13*
14* You should have received a copy of the GNU General Public
15* License along with this program; if not, write to the
16* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17* Boston, MA 02110-1301 USA
18*/
19
20public class BackingFileState {
21    public string filepath;
22    public int64 filesize;
23    public int64 modification_time;
24    public string? md5;
25
26    public BackingFileState (string filepath, int64 filesize, int64 modification_time, string? md5) {
27        this.filepath = filepath;
28        this.filesize = filesize;
29        this.modification_time = modification_time;
30        this.md5 = md5;
31    }
32
33    public BackingFileState.from_photo_row (BackingPhotoRow photo_row, string? md5) {
34        this.filepath = photo_row.filepath;
35        this.filesize = photo_row.filesize;
36        this.modification_time = photo_row.timestamp;
37        this.md5 = md5;
38    }
39
40    public File get_file () {
41        return File.new_for_path (filepath);
42    }
43}
44
45public abstract class MediaSource : ThumbnailSource, Indexable {
46    public virtual signal void master_replaced (File old_file, File new_file) {
47    }
48
49    private Event? event = null;
50    private string? indexable_keywords = null;
51
52    protected MediaSource (int64 object_id = INVALID_OBJECT_ID) {
53        base (object_id);
54    }
55
56    protected static inline uint64 internal_add_flags (uint64 flags, uint64 selector) {
57        return (flags | selector);
58    }
59
60    protected static inline uint64 internal_remove_flags (uint64 flags, uint64 selector) {
61        return (flags & ~selector);
62    }
63
64    protected static inline bool internal_is_flag_set (uint64 flags, uint64 selector) {
65        return ((flags & selector) != 0);
66    }
67
68    protected virtual void notify_master_replaced (File old_file, File new_file) {
69        master_replaced (old_file, new_file);
70    }
71
72    protected override void notify_altered (Alteration alteration) {
73        Alteration local = alteration;
74
75        if (local.has_detail ("metadata", "name") || local.has_detail ("backing", "master")) {
76            update_indexable_keywords ();
77            local = local.compress (new Alteration ("indexable", "keywords"));
78        }
79
80        base.notify_altered (local);
81    }
82
83    // use this method as a kind of post-constructor initializer; it means the DataSource has been
84    // added or removed to a SourceCollection.
85    protected override void notify_membership_changed (DataCollection? collection) {
86        if (collection != null && indexable_keywords == null) {
87            // don't fire the alteration here, as the MediaSource is only being added to its
88            // SourceCollection
89            update_indexable_keywords ();
90        }
91
92        base.notify_membership_changed (collection);
93    }
94
95    private void update_indexable_keywords () {
96        string[] indexables = new string[3];
97        indexables[0] = get_title ();
98        indexables[1] = get_basename ();
99        indexables[2] = get_comment ();
100
101        indexable_keywords = prepare_indexable_strings (indexables);
102    }
103
104    public unowned string? get_indexable_keywords () {
105        return indexable_keywords;
106    }
107
108    protected abstract bool set_event_id (EventID id);
109
110    protected bool delete_original_file () {
111        bool ret = false;
112        File file = get_master_file ();
113
114        try {
115            ret = file.delete (null);
116        } catch (Error err) {
117            // log error but don't abend, as this is not fatal to operation (also, could be
118            // the photo is removed because it could not be found during a verify)
119            message ("Unable to delete original photo %s: %s", file.get_path (), err.message);
120        }
121
122        // remove empty directories corresponding to imported path, but only if file is located
123        // inside the user's Pictures directory
124        if (file.has_prefix (AppDirs.get_import_dir ())) {
125            File parent = file;
126            while (!parent.equal (AppDirs.get_import_dir ())) {
127                parent = parent.get_parent ();
128                if ((parent == null) || (parent.equal (AppDirs.get_import_dir ())))
129                    break;
130
131                try {
132                    if (!query_is_directory_empty (parent))
133                        break;
134                } catch (Error err) {
135                    warning ("Unable to query file info for %s: %s", parent.get_path (), err.message);
136
137                    break;
138                }
139
140                try {
141                    parent.delete (null);
142                    debug ("Deleted empty directory %s", parent.get_path ());
143                } catch (Error err) {
144                    // again, log error but don't abend
145                    message ("Unable to delete empty directory %s: %s", parent.get_path (),
146                             err.message);
147                }
148            }
149        }
150
151        return ret;
152    }
153
154    public override string get_name () {
155        string? title = get_title ();
156
157        return is_string_empty (title) ? get_basename () : title;
158    }
159
160    public virtual string get_basename () {
161        return get_file ().get_basename ();
162    }
163
164    public abstract File get_file ();
165    public abstract File get_master_file ();
166    public abstract uint64 get_master_filesize ();
167    public abstract uint64 get_filesize ();
168    public abstract int64 get_timestamp ();
169
170    // Must return at least one, for the master file.
171    public abstract BackingFileState[] get_backing_files_state ();
172
173    public abstract string? get_title ();
174    public abstract string? get_comment ();
175    public abstract void set_title (string? title);
176    public abstract bool set_comment (string? comment);
177
178    public static string? prep_title (string? title) {
179        return prepare_input_text (title,
180                                   PrepareInputTextOptions.DEFAULT & ~PrepareInputTextOptions.EMPTY_IS_NULL, DEFAULT_USER_TEXT_INPUT_LENGTH);
181    }
182
183    public static string? prep_comment (string? comment) {
184        return prepare_input_text (comment,
185                                   PrepareInputTextOptions.DEFAULT & ~PrepareInputTextOptions.STRIP_CRLF & ~PrepareInputTextOptions.EMPTY_IS_NULL, -1);
186    }
187
188    public abstract Dimensions get_dimensions (Photo.Exception disallowed_steps = Photo.Exception.NONE);
189
190    // A preview pixbuf is one that can be quickly generated and scaled as a preview. For media
191    // type that support transformations (i.e. photos) it is fully transformed.
192    //
193    // Note that an unscaled scaling is not considered a performance-killer for this method,
194    // although the quality of the pixbuf may be quite poor compared to the actual unscaled
195    // transformed pixbuf.
196    public abstract Gdk.Pixbuf get_preview_pixbuf (Scaling scaling) throws Error;
197
198    public abstract bool is_trashed ();
199    public abstract void trash ();
200    public abstract void untrash ();
201
202    public abstract bool is_offline ();
203    public abstract void mark_offline ();
204    public abstract void mark_online ();
205
206    public abstract string get_master_md5 ();
207
208    // WARNING: some child classes of MediaSource (e.g. Photo) implement this method in a
209    //          non-thread safe manner for efficiency.
210    public abstract EventID get_event_id ();
211
212    public Event? get_event () {
213        if (event != null)
214            return event;
215
216        EventID event_id = get_event_id ();
217        if (!event_id.is_valid ())
218            return null;
219
220        event = Event.global.fetch (event_id);
221
222        return event;
223    }
224
225    public bool set_event (Event? new_event) {
226        EventID event_id = (new_event != null) ? new_event.get_event_id () : EventID ();
227        if (get_event_id ().id == event_id.id)
228            return true;
229
230        bool committed = set_event_id (event_id);
231        if (committed) {
232            if (event != null)
233                event.detach (this);
234
235            if (new_event != null)
236                new_event.attach (this);
237
238            event = new_event;
239
240            notify_altered (new Alteration ("metadata", "event"));
241        }
242
243        return committed;
244    }
245
246    public static void set_many_to_event (Gee.Collection<MediaSource> media_sources, Event? event,
247                                          TransactionController controller) throws Error {
248        EventID event_id = (event != null) ? event.get_event_id () : EventID ();
249
250        controller.begin ();
251
252        foreach (MediaSource media in media_sources) {
253            Event? old_event = media.get_event ();
254            if (old_event != null)
255                old_event.detach (media);
256
257            media.set_event_id (event_id);
258            media.event = event;
259        }
260
261        if (event != null)
262            event.attach_many (media_sources);
263
264        Alteration alteration = new Alteration ("metadata", "event");
265        foreach (MediaSource media in media_sources)
266            media.notify_altered (alteration);
267
268        controller.commit ();
269    }
270
271    public abstract int64 get_exposure_time ();
272
273    public abstract ImportID get_import_id ();
274}
275
276public class MediaSourceHoldingTank : DatabaseSourceHoldingTank {
277    private Gee.HashMap<File, MediaSource> master_file_map = new Gee.HashMap<File, MediaSource> (
278        file_hash, file_equal);
279
280    public MediaSourceHoldingTank (MediaSourceCollection sources,
281                                   SourceHoldingTank.CheckToKeep check_to_keep, GetSourceDatabaseKey get_key) {
282        base (sources, check_to_keep, get_key);
283    }
284
285    public MediaSource? fetch_by_master_file (File file) {
286        return master_file_map.get (file);
287    }
288
289    public MediaSource? fetch_by_md5 (string md5) {
290        foreach (MediaSource source in master_file_map.values) {
291            if (source.get_master_md5 () == md5) {
292                return source;
293            }
294        }
295
296        return null;
297    }
298
299    protected override void notify_contents_altered (Gee.Collection<DataSource>? added,
300            Gee.Collection<DataSource>? removed) {
301        if (added != null) {
302            foreach (DataSource source in added) {
303                MediaSource media_source = (MediaSource) source;
304                master_file_map.set (media_source.get_master_file (), media_source);
305                media_source.master_replaced.connect (on_master_source_replaced);
306            }
307        }
308
309        if (removed != null) {
310            foreach (DataSource source in removed) {
311                MediaSource media_source = (MediaSource) source;
312                bool is_removed = master_file_map.unset (media_source.get_master_file ());
313                assert (is_removed);
314                media_source.master_replaced.disconnect (on_master_source_replaced);
315            }
316        }
317
318        base.notify_contents_altered (added, removed);
319    }
320
321    private void on_master_source_replaced (MediaSource media_source, File old_file, File new_file) {
322        bool removed = master_file_map.unset (old_file);
323        assert (removed);
324
325        master_file_map.set (new_file, media_source);
326    }
327}
328
329// This class is good for any MediaSourceCollection that is backed by a DatabaseTable (which should
330// be all of them, but if not, they should construct their own implementation).
331public class MediaSourceTransactionController : TransactionController {
332    private MediaSourceCollection sources;
333
334    public MediaSourceTransactionController (MediaSourceCollection sources) {
335        this.sources = sources;
336    }
337
338    protected override void begin_impl () throws Error {
339        DatabaseTable.begin_transaction ();
340        sources.freeze_notifications ();
341    }
342
343    protected override void commit_impl () throws Error {
344        sources.thaw_notifications ();
345        DatabaseTable.commit_transaction ();
346    }
347}
348
349public abstract class MediaSourceCollection : DatabaseSourceCollection {
350    public abstract TransactionController transaction_controller {
351        get;
352    }
353
354    private MediaSourceHoldingTank trashcan = null;
355    private MediaSourceHoldingTank offline_bin = null;
356    private Gee.HashMap<File, MediaSource> by_master_file = new Gee.HashMap<File, MediaSource> (
357        file_hash, file_equal);
358    private Gee.MultiMap < ImportID?, MediaSource > import_rolls =
359        new Gee.TreeMultiMap < ImportID?, MediaSource > (ImportID.compare_func);
360    private Gee.TreeSet < ImportID?> sorted_import_ids = new Gee.TreeSet < ImportID?> (ImportID.compare_func);
361    private Gee.Set<MediaSource> flagged = new Gee.HashSet<MediaSource> ();
362
363    // This signal is fired when MediaSources are added to the collection due to a successful import.
364    // "items-added" and "contents-altered" will follow.
365    public virtual signal void media_import_starting (Gee.Collection<MediaSource> media) {
366    }
367
368    // This signal is fired when MediaSources have been added to the collection due to a successful
369    // import and import postprocessing has completed (such as adding an import Photo to its Tags).
370    // Thus, signals that have already been fired (in this order) are "media-imported", "items-added",
371    // "contents-altered" before this signal.
372    public virtual signal void media_import_completed (Gee.Collection<MediaSource> media) {
373    }
374
375    public virtual signal void master_file_replaced (MediaSource media, File old_file, File new_file) {
376    }
377
378    public virtual signal void trashcan_contents_altered (Gee.Collection<MediaSource>? added,
379            Gee.Collection<MediaSource>? removed) {
380    }
381
382    public virtual signal void import_roll_altered () {
383    }
384
385    public virtual signal void offline_contents_altered (Gee.Collection<MediaSource>? added,
386            Gee.Collection<MediaSource>? removed) {
387    }
388
389    public virtual signal void flagged_contents_altered () {
390    }
391
392    protected MediaSourceCollection (string name, GetSourceDatabaseKey source_key_func) {
393        base (name, source_key_func);
394
395        trashcan = create_trashcan ();
396        offline_bin = create_offline_bin ();
397    }
398
399    public static void filter_media (Gee.Collection<MediaSource> media,
400                                     Gee.Collection<LibraryPhoto>? photos, Gee.Collection<Video>? videos) {
401        foreach (MediaSource source in media) {
402            if (photos != null && source is LibraryPhoto)
403                photos.add ((LibraryPhoto) source);
404            else if (videos != null && source is Video)
405                videos.add ((Video) source);
406            else if (photos != null || videos != null)
407                warning ("Unrecognized media: %s", source.to_string ());
408        }
409    }
410
411    public static void count_media (Gee.Collection<MediaSource> media, out int photo_count,
412                                    out int video_count) {
413        var photos = new Gee.ArrayList<LibraryPhoto> ();
414        var videos = new Gee.ArrayList<Video> ();
415
416        filter_media (media, photos, videos);
417
418        photo_count = photos.size;
419        video_count = videos.size;
420    }
421
422    public static bool has_photo (Gee.Collection<MediaSource> media) {
423        foreach (MediaSource current_media in media) {
424            if (current_media is Photo) {
425                return true;
426            }
427        }
428
429        return false;
430    }
431
432    public static bool has_video (Gee.Collection<MediaSource> media) {
433        foreach (MediaSource current_media in media) {
434            if (current_media is Video) {
435                return true;
436            }
437        }
438
439        return false;
440    }
441
442    protected abstract MediaSourceHoldingTank create_trashcan ();
443
444    protected abstract MediaSourceHoldingTank create_offline_bin ();
445
446    public abstract MediaMonitor create_media_monitor (Workers workers, Cancellable cancellable);
447
448    public abstract string get_typename ();
449
450    public abstract bool is_file_recognized (File file);
451
452    public MediaSourceHoldingTank get_trashcan () {
453        return trashcan;
454    }
455
456    public MediaSourceHoldingTank get_offline_bin () {
457        return offline_bin;
458    }
459
460    // NOTE: numeric id's are not unique throughout the system -- they're only unique
461    //       per media type. So a MediaSourceCollection should only ever hold media
462    //       of the same type.
463    protected abstract MediaSource? fetch_by_numeric_id (int64 numeric_id);
464
465    protected virtual void notify_import_roll_altered () {
466        import_roll_altered ();
467    }
468
469    protected virtual void notify_flagged_contents_altered () {
470        flagged_contents_altered ();
471    }
472
473    protected virtual void notify_media_import_starting (Gee.Collection<MediaSource> media) {
474        media_import_starting (media);
475    }
476
477    protected virtual void notify_media_import_completed (Gee.Collection<MediaSource> media) {
478        media_import_completed (media);
479    }
480
481    protected override void items_altered (Gee.Map<DataObject, Alteration> items) {
482        Gee.ArrayList<MediaSource> to_trashcan = null;
483        Gee.ArrayList<MediaSource> to_offline = null;
484        bool flagged_altered = false;
485        foreach (DataObject object in items.keys) {
486            Alteration alteration = items.get (object);
487            MediaSource source = (MediaSource) object;
488
489            if (!alteration.has_subject ("metadata"))
490                continue;
491
492            if (source.is_trashed () && !get_trashcan ().contains (source)) {
493                if (to_trashcan == null)
494                    to_trashcan = new Gee.ArrayList<MediaSource> ();
495
496                to_trashcan.add (source);
497
498                // sources can only be in trashcan or offline -- not both
499                continue;
500            }
501
502            if (source.is_offline () && !get_offline_bin ().contains (source)) {
503                if (to_offline == null)
504                    to_offline = new Gee.ArrayList<MediaSource> ();
505
506                to_offline.add (source);
507            }
508
509            Flaggable? flaggable = source as Flaggable;
510            if (flaggable != null) {
511                if (flaggable.is_flagged ())
512                    flagged_altered = flagged.add (source) || flagged_altered;
513                else
514                    flagged_altered = flagged.remove (source) || flagged_altered;
515            }
516        }
517
518        if (to_trashcan != null)
519            get_trashcan ().unlink_and_hold (to_trashcan);
520
521        if (to_offline != null)
522            get_offline_bin ().unlink_and_hold (to_offline);
523
524        if (flagged_altered)
525            notify_flagged_contents_altered ();
526
527        base.items_altered (items);
528    }
529
530    protected override void notify_contents_altered (Gee.Iterable<DataObject>? added,
531            Gee.Iterable<DataObject>? removed) {
532        bool import_roll_changed = false;
533        bool flagged_altered = false;
534        if (added != null) {
535            foreach (DataObject object in added) {
536                MediaSource media = (MediaSource) object;
537
538                by_master_file.set (media.get_master_file (), media);
539                media.master_replaced.connect (on_master_replaced);
540
541                ImportID import_id = media.get_import_id ();
542                if (import_id.is_valid ()) {
543                    sorted_import_ids.add (import_id);
544                    import_rolls.set (import_id, media);
545
546                    import_roll_changed = true;
547                }
548
549                Flaggable? flaggable = media as Flaggable;
550                if (flaggable != null ) {
551                    if (flaggable.is_flagged ())
552                        flagged_altered = flagged.add (media) || flagged_altered;
553                    else
554                        flagged_altered = flagged.remove (media) || flagged_altered;
555                }
556            }
557        }
558
559        if (removed != null) {
560            foreach (DataObject object in removed) {
561                MediaSource media = (MediaSource) object;
562
563                bool is_removed = by_master_file.unset (media.get_master_file ());
564                assert (is_removed);
565                media.master_replaced.disconnect (on_master_replaced);
566
567                ImportID import_id = media.get_import_id ();
568                if (import_id.is_valid ()) {
569                    is_removed = import_rolls.remove (import_id, media);
570                    assert (is_removed);
571                    if (!import_rolls.contains (import_id))
572                        sorted_import_ids.remove (import_id);
573
574                    import_roll_changed = true;
575                }
576
577                flagged_altered = flagged.remove (media) || flagged_altered;
578            }
579        }
580
581        if (import_roll_changed)
582            notify_import_roll_altered ();
583
584        if (flagged_altered)
585            notify_flagged_contents_altered ();
586
587        base.notify_contents_altered (added, removed);
588    }
589
590    private void on_master_replaced (MediaSource media, File old_file, File new_file) {
591        bool is_removed = by_master_file.unset (old_file);
592        assert (is_removed);
593
594        by_master_file.set (new_file, media);
595
596        master_file_replaced (media, old_file, new_file);
597    }
598
599    public MediaSource? fetch_by_master_file (File file) {
600        return by_master_file.get (file);
601    }
602
603    public virtual MediaSource? fetch_by_source_id (string source_id) {
604        string[] components = source_id.split ("-");
605        assert (components.length == 2);
606
607        return fetch_by_numeric_id (parse_int64 (components[1], 16));
608    }
609
610    public abstract Gee.Collection<string> get_event_source_ids (EventID event_id);
611
612    public Gee.Collection<MediaSource> get_trashcan_contents () {
613        return (Gee.Collection<MediaSource>) get_trashcan ().get_all ();
614    }
615
616    public Gee.Collection<MediaSource> get_offline_bin_contents () {
617        return (Gee.Collection<MediaSource>) get_offline_bin ().get_all ();
618    }
619
620    public Gee.Collection<MediaSource> get_flagged () {
621        return flagged.read_only_view;
622    }
623
624    // The returned set of ImportID's is sorted from oldest to newest.
625    public Gee.SortedSet < ImportID?> get_import_roll_ids () {
626        return sorted_import_ids;
627    }
628
629    public ImportID? get_last_import_id () {
630        return sorted_import_ids.size != 0 ? sorted_import_ids.last () : null;
631    }
632
633    public Gee.Collection < MediaSource?>? get_import_roll (ImportID import_id) {
634        return import_rolls.get (import_id);
635    }
636
637    public void add_many_to_trash (Gee.Collection<MediaSource> sources) {
638        get_trashcan ().add_many (sources);
639    }
640
641    public void add_many_to_offline (Gee.Collection<MediaSource> sources) {
642        get_offline_bin ().add_many (sources);
643    }
644
645    public int get_trashcan_count () {
646        return get_trashcan ().get_count ();
647    }
648
649    // This method should be used in place of add_many () when adding MediaSources due to a successful
650    // import.  This function fires appropriate signals and calls add_many (), so the signals
651    // associated with that call will be fired too.
652    public virtual void import_many (Gee.Collection<MediaSource> media) {
653        notify_media_import_starting (media);
654
655        add_many (media);
656
657        postprocess_imported_media (media);
658
659        notify_media_import_completed (media);
660    }
661
662    // Child classes can override this method to perform postprocessing on a imported media, such
663    // as associating them with tags or events.
664    protected virtual void postprocess_imported_media (Gee.Collection<MediaSource> media) {
665    }
666
667    // This operation cannot be cancelled; the return value of the ProgressMonitor is ignored.
668    // Note that delete_backing dictates whether or not the photos are tombstoned (if deleted,
669    // tombstones are not created).
670    public void remove_from_app (Gee.Collection<MediaSource>? sources, bool delete_backing,
671                                 ProgressMonitor? monitor = null, Gee.List<MediaSource>? not_removed = null) {
672        assert (sources != null);
673        // only tombstone if the backing is not being deleted
674        Gee.HashSet<MediaSource> to_tombstone = !delete_backing ? new Gee.HashSet<MediaSource> () : null;
675
676        // separate photos into two piles: those in the trash and those not
677        Gee.ArrayList<MediaSource> trashed = new Gee.ArrayList<MediaSource> ();
678        Gee.ArrayList<MediaSource> offlined = new Gee.ArrayList<MediaSource> ();
679        Gee.ArrayList<MediaSource> not_trashed = new Gee.ArrayList<MediaSource> ();
680        foreach (MediaSource source in sources) {
681            if (source.is_trashed ())
682                trashed.add (source);
683            else if (source.is_offline ())
684                offlined.add (source);
685            else
686                not_trashed.add (source);
687
688            if (to_tombstone != null)
689                to_tombstone.add (source);
690        }
691
692        int total_count = sources.size;
693        assert (total_count == (trashed.size + offlined.size + not_trashed.size));
694
695        // use an aggregate progress monitor, as it's possible there are three steps here
696        AggregateProgressMonitor agg_monitor = null;
697        if (monitor != null) {
698            agg_monitor = new AggregateProgressMonitor (total_count, monitor);
699            monitor = agg_monitor.monitor;
700        }
701
702        if (trashed.size > 0)
703            get_trashcan ().destroy_orphans (trashed, delete_backing, monitor, not_removed);
704
705        if (offlined.size > 0)
706            get_offline_bin ().destroy_orphans (offlined, delete_backing, monitor, not_removed);
707
708        // untrashed media sources may be destroyed outright
709        if (not_trashed.size > 0)
710            destroy_marked (mark_many (not_trashed), delete_backing, monitor, not_removed);
711
712        if (to_tombstone != null && to_tombstone.size > 0) {
713            try {
714                Tombstone.entomb_many_sources (to_tombstone, Tombstone.Reason.REMOVED_BY_USER);
715            } catch (DatabaseError err) {
716                AppWindow.database_error (err);
717            }
718        }
719    }
720
721    // Deletes (i.e. not trashes) the backing files.
722    // Note: must be removed from DB first.
723    public void delete_backing_files (Gee.Collection<MediaSource> sources,
724                                      ProgressMonitor? monitor = null, Gee.List<MediaSource>? not_deleted = null) {
725        int total_count = sources.size;
726        int i = 1;
727
728        foreach (MediaSource source in sources) {
729            File file = source.get_file ();
730            try {
731                file.delete (null);
732            } catch (Error err) {
733                // Note: we may get an exception even though the delete succeeded.
734                debug ("Exception deleting file %s: %s", file.get_path (), err.message);
735            }
736
737            bool deleted = !file.query_exists ();
738            if (!deleted && null != not_deleted) {
739                not_deleted.add (source);
740            }
741
742            if (monitor != null) {
743                monitor (i, total_count);
744            }
745            i++;
746        }
747    }
748}
749
750public class MediaCollectionRegistry {
751    private const int LIBRARY_MONITOR_START_DELAY_MSEC = 1000;
752
753    private static MediaCollectionRegistry? instance = null;
754
755    private Gee.ArrayList<MediaSourceCollection> all = new Gee.ArrayList<MediaSourceCollection> ();
756    private Gee.HashMap<string, MediaSourceCollection> by_typename =
757        new Gee.HashMap<string, MediaSourceCollection> ();
758
759    private static GLib.Settings file_settings;
760
761    private MediaCollectionRegistry () {
762        ((Photos.Application) GLib.Application.get_default ()).init_done.connect (on_init_done);
763    }
764
765    ~MediaCollectionRegistry () {
766        ((Photos.Application) GLib.Application.get_default ()).init_done.disconnect (on_init_done);
767    }
768
769    private void on_init_done () {
770        // install the default library monitor
771        LibraryMonitor library_monitor = new LibraryMonitor (AppDirs.get_import_dir (), true,
772                !CommandlineOptions.no_runtime_monitoring);
773
774        LibraryMonitorPool.get_instance ().replace (library_monitor, LIBRARY_MONITOR_START_DELAY_MSEC);
775    }
776
777    public static void init () {
778        file_settings = new GLib.Settings (GSettingsConfigurationEngine.FILES_PREFS_SCHEMA_NAME);
779        instance = new MediaCollectionRegistry ();
780        file_settings.changed.connect (on_config_changed);
781    }
782
783    private static void on_config_changed (string key) {
784        if (key != "import-dir") {
785            return;
786        }
787
788        File import_dir = AppDirs.get_import_dir ();
789
790        LibraryMonitor? current = LibraryMonitorPool.get_instance ().get_monitor ();
791        if (current != null && current.get_root ().equal (import_dir))
792            return;
793
794        LibraryMonitor replacement = new LibraryMonitor (import_dir, true,
795                !CommandlineOptions.no_runtime_monitoring);
796        LibraryMonitorPool.get_instance ().replace (replacement, LIBRARY_MONITOR_START_DELAY_MSEC);
797    }
798
799    public static MediaCollectionRegistry get_instance () {
800        return instance;
801    }
802
803    public static string get_typename_from_source_id (string source_id) {
804        // we have to special-case photos because their source id format is non-standard. this
805        // is due to a historical quirk.
806        if (source_id.has_prefix (Photo.TYPENAME)) {
807            return Photo.TYPENAME;
808        } else {
809            string[] components = source_id.split ("-");
810            assert (components.length == 2);
811
812            return components[0];
813        }
814    }
815
816    public void register_collection (MediaSourceCollection collection) {
817        all.add (collection);
818        by_typename.set (collection.get_typename (), collection);
819    }
820
821    // NOTE: going forward, please use get_collection( ) and get_all_collections( ) to get the
822    //       collection associated with a specific media type or to get all registered collections,
823    //       respectively, instead of explicitly referencing Video.global and LibraryPhoto.global.
824    //       This will make it *much* easier to add new media types in the future.
825    public MediaSourceCollection? get_collection (string typename) {
826        return by_typename.get (typename);
827    }
828
829    public Gee.Collection<MediaSourceCollection> get_all () {
830        return all.read_only_view;
831    }
832
833    public void freeze_all () {
834        foreach (MediaSourceCollection sources in get_all ())
835            sources.freeze_notifications ();
836    }
837
838    public void thaw_all () {
839        foreach (MediaSourceCollection sources in get_all ())
840            sources.thaw_notifications ();
841    }
842
843    public void begin_transaction_on_all () {
844        foreach (MediaSourceCollection sources in get_all ())
845            sources.transaction_controller.begin ();
846    }
847
848    public void commit_transaction_on_all () {
849        foreach (MediaSourceCollection sources in get_all ())
850            sources.transaction_controller.commit ();
851    }
852
853    public MediaSource? fetch_media (string source_id) {
854        string typename = get_typename_from_source_id (source_id);
855
856        MediaSourceCollection? collection = get_collection (typename);
857        if (collection == null) {
858            critical ("source id '%s' has unrecognized media type '%s'", source_id, typename);
859            return null;
860        }
861
862        return collection.fetch_by_source_id (source_id);
863    }
864
865    public ImportID? get_last_import_id () {
866        ImportID last_import_id = ImportID ();
867
868        foreach (MediaSourceCollection current_collection in get_all ()) {
869            ImportID? current_import_id = current_collection.get_last_import_id ();
870
871            if (current_import_id == null)
872                continue;
873
874            if (current_import_id.id > last_import_id.id)
875                last_import_id = current_import_id;
876        }
877
878        // VALA: can't use the ternary operator here because of bug 616897 : "Mixed nullability in
879        //       ternary operator fails"
880        if (last_import_id.id == ImportID.INVALID)
881            return null;
882        else
883            return last_import_id;
884    }
885
886    public Gee.Collection<string> get_source_ids_for_event_id (EventID event_id) {
887        Gee.ArrayList<string> result = new Gee.ArrayList<string> ();
888
889        foreach (MediaSourceCollection current_collection in get_all ()) {
890            result.add_all (current_collection.get_event_source_ids (event_id));
891        }
892
893        return result;
894    }
895
896    public MediaSourceCollection? get_collection_for_file (File file) {
897        foreach (MediaSourceCollection collection in get_all ()) {
898            if (collection.is_file_recognized (file))
899                return collection;
900        }
901
902        return null;
903    }
904
905    public bool is_valid_source_id (string? source_id) {
906        if (is_string_empty (source_id)) {
907            return false;
908        }
909        return (source_id.has_prefix (Photo.TYPENAME) || source_id.has_prefix (Video.TYPENAME + "-"));
910    }
911}
912