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