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 errordomain VideoError { 21 FILE, // there's a problem reading the video container file (doesn't exist, no read 22 // permission, etc.) 23 24 CONTENTS, // we can read the container file but its contents are indecipherable (no codec, 25 // malformed data, etc.) 26} 27 28public class VideoImportParams { 29 // IN: 30 public File file; 31 public ImportID import_id = ImportID (); 32 public string? md5; 33 public int64 exposure_time_override; 34 35 // IN/OUT: 36 public Thumbnails? thumbnails; 37 38 // OUT: 39 public VideoRow row = new VideoRow (); 40 41 public VideoImportParams (File file, ImportID import_id, string? md5, 42 Thumbnails? thumbnails = null, int64 exposure_time_override = 0) { 43 this.file = file; 44 this.import_id = import_id; 45 this.md5 = md5; 46 this.thumbnails = thumbnails; 47 this.exposure_time_override = exposure_time_override; 48 } 49} 50 51public class VideoReader { 52 private const double UNKNOWN_CLIP_DURATION = -1.0; 53 private const uint THUMBNAILER_TIMEOUT = 10000; // In milliseconds. 54 55 // File extensions for video containers that pack only metadata as per the AVCHD spec 56 private const string[] METADATA_ONLY_FILE_EXTENSIONS = { "bdm", "bdmv", "cpi", "mpl" }; 57 58 private double clip_duration = UNKNOWN_CLIP_DURATION; 59 private Gdk.Pixbuf preview_frame = null; 60 private File file = null; 61 private GLib.Pid thumbnailer_pid = 0; 62 public int64 timestamp { 63 get; 64 private set; 65 default = -1; 66 } 67 68 public VideoReader (File file) { 69 this.file = file; 70 } 71 72 public static bool is_supported_video_file (File file) { 73 return is_supported_video_filename (file.get_basename ()); 74 } 75 76 public static bool is_supported_video_filename (string filename) { 77 string mime_type; 78 mime_type = ContentType.guess (filename, new uchar[0], null); 79 if (mime_type.length >= 6 && mime_type[0:6] == "video/") { 80 string? extension = null; 81 string? name = null; 82 disassemble_filename (filename, out name, out extension); 83 84 if (extension == null) 85 return true; 86 87 foreach (string s in METADATA_ONLY_FILE_EXTENSIONS) { 88 if (utf8_ci_compare (s, extension) == 0) 89 return false; 90 } 91 92 return true; 93 } else { 94 return false; 95 } 96 } 97 98 public static ImportResult prepare_for_import (VideoImportParams params) { 99#if MEASURE_IMPORT 100 Timer total_time = new Timer (); 101#endif 102 File file = params.file; 103 104 FileInfo info = null; 105 try { 106 info = file.query_info (DirectoryMonitor.SUPPLIED_ATTRIBUTES, 107 FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null); 108 } catch (Error err) { 109 return ImportResult.FILE_ERROR; 110 } 111 112 if (info.get_file_type () != FileType.REGULAR) 113 return ImportResult.NOT_A_FILE; 114 115 if (!is_supported_video_file (file)) { 116 message ("Not importing %s: file is marked as a video file but doesn't have a" + 117 "supported extension", file.get_path ()); 118 119 return ImportResult.UNSUPPORTED_FORMAT; 120 } 121 122 var timestamp = info.get_modification_date_time ().to_unix (); 123 124 // make sure params has a valid md5 125 assert (params.md5 != null); 126 127 int64 exposure_time = params.exposure_time_override; 128 string title = ""; 129 string comment = ""; 130 131 VideoReader reader = new VideoReader (file); 132 bool is_interpretable = true; 133 double clip_duration = 0.0; 134 Gdk.Pixbuf preview_frame = reader.read_preview_frame (); 135 try { 136 clip_duration = reader.read_clip_duration (); 137 } catch (VideoError err) { 138 if (err is VideoError.FILE) { 139 return ImportResult.FILE_ERROR; 140 } else if (err is VideoError.CONTENTS) { 141 is_interpretable = false; 142 clip_duration = 0.0; 143 } else { 144 error ("can't prepare video for import: an unknown kind of video error occurred"); 145 } 146 } 147 148 try { 149 VideoMetadata metadata = reader.read_metadata (); 150 MetadataDateTime? creation_date_time = metadata.get_creation_date_time (); 151 152 if (creation_date_time != null && creation_date_time.get_timestamp () != 0) 153 exposure_time = creation_date_time.get_timestamp (); 154 155 string? video_title = metadata.get_title (); 156 string? video_comment = metadata.get_comment (); 157 if (video_title != null) 158 title = video_title; 159 if (video_comment != null) 160 comment = video_comment; 161 } catch (Error err) { 162 warning ("Unable to read video metadata: %s", err.message); 163 } 164 165 if (exposure_time == 0) { 166 // Use time reported by Gstreamer, if available. 167 exposure_time = (reader.timestamp > 0 ? reader.timestamp : 0); 168 } 169 170 params.row.video_id = VideoID (); 171 params.row.filepath = file.get_path (); 172 params.row.filesize = info.get_size (); 173 params.row.timestamp = timestamp; 174 params.row.width = preview_frame.width; 175 params.row.height = preview_frame.height; 176 params.row.clip_duration = clip_duration; 177 params.row.is_interpretable = is_interpretable; 178 params.row.exposure_time = exposure_time; 179 params.row.import_id = params.import_id; 180 params.row.event_id = EventID (); 181 params.row.md5 = params.md5; 182 params.row.time_created = 0; 183 params.row.title = title; 184 params.row.comment = comment; 185 params.row.backlinks = ""; 186 params.row.time_reimported = 0; 187 params.row.flags = 0; 188 189 if (params.thumbnails != null) { 190 params.thumbnails = new Thumbnails (); 191 ThumbnailCache.generate_for_video_frame (params.thumbnails, preview_frame); 192 } 193 194#if MEASURE_IMPORT 195 debug ("IMPORT: total time to import video = %lf", total_time.elapsed ()); 196#endif 197 return ImportResult.SUCCESS; 198 } 199 200 private void read_internal () throws VideoError { 201 if (!does_file_exist ()) 202 throw new VideoError.FILE ("video file '%s' does not exist or is inaccessible".printf ( 203 file.get_path ())); 204 205 try { 206 Gst.PbUtils.Discoverer d = new Gst.PbUtils.Discoverer ((Gst.ClockTime) (Gst.SECOND * 5)); 207 Gst.PbUtils.DiscovererInfo info = d.discover_uri (file.get_uri ()); 208 209 clip_duration = ((double) info.get_duration ()) / 1000000000.0; 210 211 // Get creation time. 212 // TODO: Note that TAG_DATE can be changed to TAG_DATE_TIME in the future 213 // (and the corresponding output struct) in order to implement #2836. 214 Date? video_date = null; 215 if (info.get_tags () != null && info.get_tags ().get_date (Gst.Tags.DATE, out video_date)) { 216 217 timestamp = new DateTime.local (video_date.get_year (), video_date.get_month (), 218 video_date.get_day (), 0, 0, 0).to_unix (); 219 } 220 } catch (Error e) { 221 debug ("Video read error: %s", e.message); 222 throw new VideoError.CONTENTS ("GStreamer couldn't extract clip information: %s" 223 .printf (e.message)); 224 } 225 } 226 227 // Used by thumbnailer () to kill the external process if need be. 228 private bool on_thumbnailer_timer () { 229 debug ("Thumbnailer timer called"); 230 if (thumbnailer_pid != 0) { 231 debug ("Killing thumbnailer process: %d", thumbnailer_pid); 232 Posix.kill (thumbnailer_pid, Posix.Signal.KILL); 233 } 234 return false; // Don't call again. 235 } 236 237 // Performs video thumbnailing. 238 // Note: not thread-safe if called from the same instance of the class. 239 private Gdk.Pixbuf? thumbnailer (string video_file) { 240 int[] pipefd = {0, 0}; 241 242 if (Posix.pipe (pipefd) < 0) { 243 warning ("Error: unable to open pipe."); 244 return null; 245 } 246 Posix.close (pipefd[1]); // Close the write end of the pipe. 247 248 // Use Photos' thumbnailer, redirect output to stdout. 249 debug ("Launching thumbnailer process: %s", AppDirs.get_thumbnailer_bin ().get_path ()); 250 string[] argv = {AppDirs.get_thumbnailer_bin ().get_path (), video_file}; 251 try { 252 GLib.Process.spawn_async_with_pipes (null, argv, null, GLib.SpawnFlags.SEARCH_PATH | 253 GLib.SpawnFlags.DO_NOT_REAP_CHILD, null, out thumbnailer_pid, null, out pipefd[0], null); 254 debug ("Spawned thumbnailer, child pid: %d", (int) thumbnailer_pid); 255 } catch (Error e) { 256 debug ("Error spawning process: %s", e.message); 257 if (thumbnailer_pid != 0) 258 GLib.Process.close_pid (thumbnailer_pid); 259 return null; 260 } 261 262 // Start timer. 263 Timeout.add (THUMBNAILER_TIMEOUT, on_thumbnailer_timer); 264 265 // Read pixbuf from stream. 266 Gdk.Pixbuf? buf = null; 267 try { 268 GLib.UnixInputStream unix_input = new GLib.UnixInputStream (pipefd[0], true); 269 buf = new Gdk.Pixbuf.from_stream (unix_input, null); 270 } catch (Error e) { 271 warning ("Error creating pixbuf: %s", e.message); 272 buf = null; 273 } 274 275 // Make sure process exited properly. 276 int child_status = 0; 277 int ret_waitpid = Posix.waitpid (thumbnailer_pid, out child_status, 0); 278 if (ret_waitpid < 0) { 279 debug ("waitpid returned error code: %d", ret_waitpid); 280 buf = null; 281 } else if (0 != posix_wexitstatus (child_status)) { 282 debug ("Thumbnailer exited with error code: %d", posix_wexitstatus (child_status)); 283 buf = null; 284 } 285 286 Posix.close (pipefd[0]); 287 GLib.Process.close_pid (thumbnailer_pid); 288 thumbnailer_pid = 0; 289 return buf; 290 } 291 292 private bool does_file_exist () { 293 return FileUtils.test (file.get_path (), FileTest.EXISTS | FileTest.IS_REGULAR); 294 } 295 296 public Gdk.Pixbuf? read_preview_frame () { 297 if (preview_frame != null) 298 return preview_frame; 299 300 if (!does_file_exist ()) 301 return null; 302 303 // Get preview frame from thumbnailer. 304 preview_frame = thumbnailer (file.get_path ()); 305 if (null == preview_frame) 306 preview_frame = Resources.get_noninterpretable_badge_pixbuf (); 307 308 return preview_frame; 309 } 310 311 public double read_clip_duration () throws VideoError { 312 if (clip_duration == UNKNOWN_CLIP_DURATION) 313 read_internal (); 314 315 return clip_duration; 316 } 317 318 public VideoMetadata read_metadata () throws Error { 319 VideoMetadata metadata = new VideoMetadata (); 320 metadata.read_from_file (File.new_for_path (file.get_path ())); 321 322 return metadata; 323 } 324} 325 326public class Video : VideoSource, Flaggable, Monitorable, Dateable { 327 public const int NO_VIDEO_INTERPRETER_STATE = -1; 328 public const string TYPENAME = "video"; 329 330 public const uint64 FLAG_TRASH = 0x0000000000000001; 331 public const uint64 FLAG_OFFLINE = 0x0000000000000002; 332 public const uint64 FLAG_FLAGGED = 0x0000000000000004; 333 334 public class InterpretableResults { 335 internal Video video; 336 internal bool update_interpretable = false; 337 internal bool is_interpretable = false; 338 internal Gdk.Pixbuf? new_thumbnail = null; 339 340 public InterpretableResults (Video video) { 341 this.video = video; 342 } 343 344 public void foreground_finish () { 345 if (update_interpretable) 346 video.set_is_interpretable (is_interpretable); 347 348 if (new_thumbnail != null) { 349 try { 350 ThumbnailCache.replace (video, ThumbnailCache.Size.BIG, new_thumbnail); 351 ThumbnailCache.replace (video, ThumbnailCache.Size.MEDIUM, new_thumbnail); 352 353 video.notify_thumbnail_altered (); 354 } catch (Error err) { 355 message ("Unable to update video thumbnails for %s: %s", video.to_string (), 356 err.message); 357 } 358 } 359 } 360 } 361 362 private static bool interpreter_state_changed; 363 private static int current_state; 364 private static bool normal_regen_complete; 365 private static bool offline_regen_complete; 366 private static GLib.Settings video_settings; 367 public static VideoSourceCollection global; 368 369 private VideoRow backing_row; 370 371 public Video (VideoRow row) { 372 this.backing_row = row; 373 374 // normalize user text 375 this.backing_row.title = prep_title (this.backing_row.title); 376 377 if (((row.flags & FLAG_TRASH) != 0) || ((row.flags & FLAG_OFFLINE) != 0)) 378 rehydrate_backlinks (global, row.backlinks); 379 } 380 381 public static void init (ProgressMonitor? monitor = null) { 382 // Must initialize static variables here. 383 // TODO: set values at declaration time once the following Vala bug is fixed: 384 // https://bugzilla.gnome.org/show_bug.cgi?id=655594 385 interpreter_state_changed = false; 386 current_state = -1; 387 normal_regen_complete = false; 388 offline_regen_complete = false; 389 video_settings = new GLib.Settings (GSettingsConfigurationEngine.VIDEO_SCHEMA_NAME); 390 391 // initialize GStreamer, but don't pass it our actual command line arguments -- we don't 392 // want our end users to be able to parameterize the GStreamer configuration 393 string[] fake_args = new string[0]; 394 unowned string[] fake_unowned_args = fake_args; 395 Gst.init (ref fake_unowned_args); 396 397 int saved_state = video_settings.get_int ("interpreter-state-cookie"); 398 current_state = (int) Gst.Registry.get ().get_feature_list_cookie (); 399 if (saved_state == NO_VIDEO_INTERPRETER_STATE) { 400 message ("interpreter state cookie not found; assuming all video thumbnails are out of date"); 401 interpreter_state_changed = true; 402 } else if (saved_state != current_state) { 403 message ("interpreter state has changed; video thumbnails may be out of date"); 404 interpreter_state_changed = true; 405 } 406 407 global = new VideoSourceCollection (); 408 409 Gee.ArrayList < VideoRow?> all = VideoTable.get_instance ().get_all (); 410 Gee.ArrayList<Video> all_videos = new Gee.ArrayList<Video> (); 411 Gee.ArrayList<Video> trashed_videos = new Gee.ArrayList<Video> (); 412 Gee.ArrayList<Video> offline_videos = new Gee.ArrayList<Video> (); 413 int count = all.size; 414 for (int ctr = 0; ctr < count; ctr++) { 415 Video video = new Video (all.get (ctr)); 416 417 if (interpreter_state_changed) 418 video.set_is_interpretable (false); 419 420 if (video.is_trashed ()) 421 trashed_videos.add (video); 422 else if (video.is_offline ()) 423 offline_videos.add (video); 424 else 425 all_videos.add (video); 426 427 if (monitor != null) 428 monitor (ctr, count); 429 } 430 431 global.add_many_to_trash (trashed_videos); 432 global.add_many_to_offline (offline_videos); 433 global.add_many (all_videos); 434 } 435 436 public static bool has_interpreter_state_changed () { 437 return interpreter_state_changed; 438 } 439 440 public static void notify_normal_thumbs_regenerated () { 441 if (normal_regen_complete) 442 return; 443 444 message ("normal video thumbnail regeneration completed"); 445 446 normal_regen_complete = true; 447 if (normal_regen_complete && offline_regen_complete) 448 save_interpreter_state (); 449 } 450 451 public static void notify_offline_thumbs_regenerated () { 452 if (offline_regen_complete) 453 return; 454 455 message ("offline video thumbnail regeneration completed"); 456 457 offline_regen_complete = true; 458 if (normal_regen_complete && offline_regen_complete) 459 save_interpreter_state (); 460 } 461 462 private static void save_interpreter_state () { 463 if (interpreter_state_changed) { 464 message ("saving video interpreter state to configuration system"); 465 466 video_settings.set_int ("interpreter-state-cookie", current_state); 467 interpreter_state_changed = false; 468 } 469 } 470 471 public static void terminate () { 472 } 473 474 public static ExporterUI? export_many (Gee.Collection<Video> videos, Exporter.CompletionCallback done, 475 bool export_in_place = false) { 476 if (videos.size == 0) 477 return null; 478 479 // in place export is relatively easy -- provide a fast, separate code path for it 480 if (export_in_place) { 481 ExporterUI temp_exporter = new ExporterUI (new Exporter.for_temp_file (videos, 482 Scaling.for_original (), ExportFormatParameters.unmodified ())); 483 temp_exporter.export (done); 484 return temp_exporter; 485 } 486 487 // one video 488 if (videos.size == 1) { 489 Video video = null; 490 foreach (Video v in videos) { 491 video = v; 492 break; 493 } 494 495 File save_as = ExportUI.choose_file (video.get_basename ()); 496 if (save_as == null) 497 return null; 498 499 try { 500 AppWindow.get_instance ().set_busy_cursor (); 501 video.export (save_as); 502 AppWindow.get_instance ().set_normal_cursor (); 503 } catch (Error err) { 504 AppWindow.get_instance ().set_normal_cursor (); 505 export_error_dialog (save_as, false); 506 } 507 508 return null; 509 } 510 511 // multiple videos 512 File export_dir = ExportUI.choose_dir (_ ("Export Videos")); 513 if (export_dir == null) 514 return null; 515 516 ExporterUI exporter = new ExporterUI (new Exporter (videos, export_dir, 517 Scaling.for_original (), ExportFormatParameters.unmodified ())); 518 exporter.export (done); 519 520 return exporter; 521 } 522 523 protected override void commit_backlinks (SourceCollection? sources, string? backlinks) { 524 try { 525 VideoTable.get_instance ().update_backlinks (get_video_id (), backlinks); 526 lock (backing_row) { 527 backing_row.backlinks = backlinks; 528 } 529 } catch (DatabaseError err) { 530 warning ("Unable to update link state for %s: %s", to_string (), err.message); 531 } 532 } 533 534 protected override bool set_event_id (EventID event_id) { 535 lock (backing_row) { 536 bool committed = VideoTable.get_instance ().set_event (backing_row.video_id, event_id); 537 538 if (committed) 539 backing_row.event_id = event_id; 540 541 return committed; 542 } 543 } 544 545 public static bool is_duplicate (File? file, string? full_md5) { 546 assert (file != null || full_md5 != null); 547#if !NO_DUPE_DETECTION 548 return VideoTable.get_instance ().has_duplicate (file, full_md5); 549#else 550 return false; 551#endif 552 } 553 554 public static ImportResult import_create (VideoImportParams params, out Video video) { 555 video = null; 556 557 // add to the database 558 try { 559 if (VideoTable.get_instance ().add (params.row).is_invalid ()) 560 return ImportResult.DATABASE_ERROR; 561 } catch (DatabaseError err) { 562 return ImportResult.DATABASE_ERROR; 563 } 564 565 // create local object but don't add to global until thumbnails generated 566 video = new Video (params.row); 567 568 return ImportResult.SUCCESS; 569 } 570 571 public static void import_failed (Video video) { 572 try { 573 VideoTable.get_instance ().remove (video.get_video_id ()); 574 } catch (DatabaseError err) { 575 AppWindow.database_error (err); 576 } 577 } 578 579 public override BackingFileState[] get_backing_files_state () { 580 BackingFileState[] backing = new BackingFileState[1]; 581 lock (backing_row) { 582 backing[0] = new BackingFileState (backing_row.filepath, backing_row.filesize, 583 backing_row.timestamp, backing_row.md5); 584 } 585 586 return backing; 587 } 588 589 public override Gdk.Pixbuf? get_thumbnail (int scale) throws Error { 590 return ThumbnailCache.fetch (this, scale); 591 } 592 593 public override string get_master_md5 () { 594 lock (backing_row) { 595 return backing_row.md5; 596 } 597 } 598 599 public override Gdk.Pixbuf get_preview_pixbuf (Scaling scaling) throws Error { 600 Gdk.Pixbuf pixbuf = get_thumbnail (ThumbnailCache.Size.BIG); 601 602 return scaling.perform_on_pixbuf (pixbuf, Gdk.InterpType.NEAREST, true); 603 } 604 605 public override Gdk.Pixbuf? create_thumbnail (int scale) throws Error { 606 VideoReader reader = new VideoReader (get_file ()); 607 Gdk.Pixbuf? frame = reader.read_preview_frame (); 608 609 return (frame != null) ? frame : Resources.get_noninterpretable_badge_pixbuf ().copy (); 610 } 611 612 public override string get_typename () { 613 return TYPENAME; 614 } 615 616 public override int64 get_instance_id () { 617 return get_video_id ().id; 618 } 619 620 public override ImportID get_import_id () { 621 lock (backing_row) { 622 return backing_row.import_id; 623 } 624 } 625 626 public override PhotoFileFormat get_preferred_thumbnail_format () { 627 return PhotoFileFormat.get_system_default_format (); 628 } 629 630 public override string? get_title () { 631 lock (backing_row) { 632 return backing_row.title; 633 } 634 } 635 636 public override void set_title (string? title) { 637 string? new_title = prep_title (title); 638 639 lock (backing_row) { 640 if (backing_row.title == new_title) 641 return; 642 643 try { 644 VideoTable.get_instance ().set_title (backing_row.video_id, new_title); 645 } catch (DatabaseError e) { 646 AppWindow.database_error (e); 647 return; 648 } 649 // if we didn't short-circuit return in the catch clause above, then the change was 650 // successfully committed to the database, so update it in the in-memory row cache 651 backing_row.title = new_title; 652 } 653 654 notify_altered (new Alteration ("metadata", "name")); 655 } 656 657 public override string? get_comment () { 658 lock (backing_row) { 659 return backing_row.comment; 660 } 661 } 662 663 public override bool set_comment (string? comment) { 664 string? new_comment = prep_title (comment); 665 666 lock (backing_row) { 667 if (backing_row.comment == new_comment) 668 return true; 669 670 try { 671 VideoTable.get_instance ().set_comment (backing_row.video_id, new_comment); 672 } catch (DatabaseError e) { 673 AppWindow.database_error (e); 674 return false; 675 } 676 // if we didn't short-circuit return in the catch clause above, then the change was 677 // successfully committed to the database, so update it in the in-memory row cache 678 backing_row.comment = new_comment; 679 } 680 681 notify_altered (new Alteration ("metadata", "comment")); 682 683 return true; 684 } 685 686 public override bool is_trashed () { 687 return is_flag_set (FLAG_TRASH); 688 } 689 690 public override bool is_offline () { 691 return is_flag_set (FLAG_OFFLINE); 692 } 693 694 public override void mark_offline () { 695 add_flags (FLAG_OFFLINE); 696 } 697 698 public override void mark_online () { 699 remove_flags (FLAG_OFFLINE); 700 701 if ((!get_is_interpretable ()) && has_interpreter_state_changed ()) 702 check_is_interpretable ().foreground_finish (); 703 } 704 705 public override void trash () { 706 add_flags (FLAG_TRASH); 707 } 708 709 public override void untrash () { 710 remove_flags (FLAG_TRASH); 711 } 712 713 public bool is_flagged () { 714 return is_flag_set (FLAG_FLAGGED); 715 } 716 717 public void mark_flagged () { 718 add_flags (FLAG_FLAGGED, new Alteration ("metadata", "flagged")); 719 } 720 721 public void mark_unflagged () { 722 remove_flags (FLAG_FLAGGED, new Alteration ("metadata", "flagged")); 723 } 724 725 public override EventID get_event_id () { 726 lock (backing_row) { 727 return backing_row.event_id; 728 } 729 } 730 731 public override string to_string () { 732 lock (backing_row) { 733 return "[%s] %s".printf (backing_row.video_id.id.to_string (), backing_row.filepath); 734 } 735 } 736 737 public VideoID get_video_id () { 738 lock (backing_row) { 739 return backing_row.video_id; 740 } 741 } 742 743 public override int64 get_exposure_time () { 744 lock (backing_row) { 745 return backing_row.exposure_time; 746 } 747 } 748 749 public void set_exposure_time (int64 time) { 750 lock (backing_row) { 751 try { 752 VideoTable.get_instance ().set_exposure_time (backing_row.video_id, time); 753 } catch (Error e) { 754 debug ("Warning - %s", e.message); 755 } 756 backing_row.exposure_time = time; 757 } 758 759 notify_altered (new Alteration ("metadata", "exposure-time")); 760 } 761 762 public Dimensions get_frame_dimensions () { 763 lock (backing_row) { 764 return Dimensions (backing_row.width, backing_row.height); 765 } 766 } 767 768 public override Dimensions get_dimensions (Photo.Exception disallowed_steps = Photo.Exception.NONE) { 769 return get_frame_dimensions (); 770 } 771 772 public override uint64 get_filesize () { 773 return get_master_filesize (); 774 } 775 776 public override uint64 get_master_filesize () { 777 lock (backing_row) { 778 return backing_row.filesize; 779 } 780 } 781 782 public override int64 get_timestamp () { 783 lock (backing_row) { 784 return backing_row.timestamp; 785 } 786 } 787 788 public void set_master_timestamp (FileInfo info) { 789 var mod_time = info.get_modification_date_time ().to_unix (); 790 791 try { 792 lock (backing_row) { 793 if (backing_row.timestamp == mod_time) 794 return; 795 796 VideoTable.get_instance ().set_timestamp (backing_row.video_id, mod_time); 797 backing_row.timestamp = mod_time; 798 } 799 } catch (DatabaseError err) { 800 AppWindow.database_error (err); 801 802 return; 803 } 804 805 notify_altered (new Alteration ("metadata", "master-timestamp")); 806 } 807 808 public string get_filename () { 809 lock (backing_row) { 810 return backing_row.filepath; 811 } 812 } 813 814 public override File get_file () { 815 return File.new_for_path (get_filename ()); 816 } 817 818 public override File get_master_file () { 819 return get_file (); 820 } 821 822 public void export (File dest_file) throws Error { 823 File source_file = File.new_for_path (get_filename ()); 824 source_file.copy (dest_file, FileCopyFlags.OVERWRITE | FileCopyFlags.TARGET_DEFAULT_PERMS, 825 null, null); 826 } 827 828 public double get_clip_duration () { 829 lock (backing_row) { 830 return backing_row.clip_duration; 831 } 832 } 833 834 public bool get_is_interpretable () { 835 lock (backing_row) { 836 return backing_row.is_interpretable; 837 } 838 } 839 840 private void set_is_interpretable (bool is_interpretable) { 841 lock (backing_row) { 842 if (backing_row.is_interpretable == is_interpretable) 843 return; 844 845 backing_row.is_interpretable = is_interpretable; 846 } 847 848 try { 849 VideoTable.get_instance ().update_is_interpretable (get_video_id (), is_interpretable); 850 } catch (DatabaseError e) { 851 AppWindow.database_error (e); 852 } 853 } 854 855 // Intended to be called from a background thread but can be called from foreground as well. 856 // Caller should call InterpretableResults.foreground_process () only from foreground thread, 857 // however 858 public InterpretableResults check_is_interpretable () { 859 InterpretableResults results = new InterpretableResults (this); 860 861 double clip_duration = -1.0; 862 Gdk.Pixbuf? preview_frame = null; 863 864 VideoReader backing_file_reader = new VideoReader (get_file ()); 865 try { 866 clip_duration = backing_file_reader.read_clip_duration (); 867 preview_frame = backing_file_reader.read_preview_frame (); 868 } catch (VideoError e) { 869 // if we catch an error on an interpretable video here, then this video is 870 // non-interpretable (e.g. its codec is not present on the users system). 871 results.update_interpretable = get_is_interpretable (); 872 results.is_interpretable = false; 873 874 return results; 875 } 876 877 // if already marked interpretable, this is only confirming what we already knew 878 if (get_is_interpretable ()) { 879 results.update_interpretable = false; 880 results.is_interpretable = true; 881 882 return results; 883 } 884 885 debug ("video %s has become interpretable", get_file ().get_basename ()); 886 887 // save this here, this can be done in background thread 888 lock (backing_row) { 889 backing_row.clip_duration = clip_duration; 890 } 891 892 results.update_interpretable = true; 893 results.is_interpretable = true; 894 results.new_thumbnail = preview_frame; 895 896 return results; 897 } 898 899 public override void destroy () { 900 VideoID video_id = get_video_id (); 901 902 ThumbnailCache.remove (this); 903 904 try { 905 VideoTable.get_instance ().remove (video_id); 906 } catch (DatabaseError err) { 907 error ("failed to remove video %s from video table", to_string ()); 908 } 909 910 base.destroy (); 911 } 912 913 protected override bool internal_delete_backing () throws Error { 914 bool ret = delete_original_file (); 915 916 // Return false if parent method failed. 917 return base.internal_delete_backing () && ret; 918 } 919 920 private void notify_flags_altered (Alteration? additional_alteration) { 921 Alteration alteration = new Alteration ("metadata", "flags"); 922 if (additional_alteration != null) 923 alteration = alteration.compress (additional_alteration); 924 925 notify_altered (alteration); 926 } 927 928 public uint64 add_flags (uint64 flags_to_add, Alteration? additional_alteration = null) { 929 uint64 new_flags; 930 lock (backing_row) { 931 new_flags = internal_add_flags (backing_row.flags, flags_to_add); 932 if (backing_row.flags == new_flags) 933 return backing_row.flags; 934 935 try { 936 VideoTable.get_instance ().set_flags (get_video_id (), new_flags); 937 } catch (DatabaseError e) { 938 AppWindow.database_error (e); 939 return backing_row.flags; 940 } 941 942 backing_row.flags = new_flags; 943 } 944 945 notify_flags_altered (additional_alteration); 946 947 return new_flags; 948 } 949 950 public uint64 remove_flags (uint64 flags_to_remove, Alteration? additional_alteration = null) { 951 uint64 new_flags; 952 lock (backing_row) { 953 new_flags = internal_remove_flags (backing_row.flags, flags_to_remove); 954 if (backing_row.flags == new_flags) 955 return backing_row.flags; 956 957 try { 958 VideoTable.get_instance ().set_flags (get_video_id (), new_flags); 959 } catch (DatabaseError e) { 960 AppWindow.database_error (e); 961 return backing_row.flags; 962 } 963 964 backing_row.flags = new_flags; 965 } 966 967 notify_flags_altered (additional_alteration); 968 969 return new_flags; 970 } 971 972 public bool is_flag_set (uint64 flag) { 973 lock (backing_row) { 974 return internal_is_flag_set (backing_row.flags, flag); 975 } 976 } 977 978 public void set_master_file (File file) { 979 string new_filepath = file.get_path (); 980 string? old_filepath = null; 981 try { 982 lock (backing_row) { 983 if (backing_row.filepath == new_filepath) 984 return; 985 986 old_filepath = backing_row.filepath; 987 988 VideoTable.get_instance ().set_filepath (backing_row.video_id, new_filepath); 989 backing_row.filepath = new_filepath; 990 } 991 } catch (DatabaseError err) { 992 AppWindow.database_error (err); 993 994 return; 995 } 996 997 assert (old_filepath != null); 998 notify_master_replaced (File.new_for_path (old_filepath), file); 999 1000 notify_altered (new Alteration.from_list ("backing:master,metadata:name")); 1001 } 1002 1003 public VideoMetadata read_metadata () throws Error { 1004 return (new VideoReader (get_file ())).read_metadata (); 1005 } 1006} 1007 1008public class VideoSourceCollection : MediaSourceCollection { 1009 public enum State { 1010 UNKNOWN, 1011 ONLINE, 1012 OFFLINE, 1013 TRASH 1014 } 1015 1016 public override TransactionController transaction_controller { 1017 get { 1018 if (_transaction_controller == null) 1019 _transaction_controller = new MediaSourceTransactionController (this); 1020 1021 return _transaction_controller; 1022 } 1023 } 1024 1025 private TransactionController _transaction_controller = null; 1026 private Gee.MultiMap < uint64?, Video > filesize_to_video = 1027 new Gee.TreeMultiMap < uint64?, Video > (uint64_compare); 1028 1029 public VideoSourceCollection () { 1030 base ("VideoSourceCollection", get_video_key); 1031 1032 get_trashcan ().contents_altered.connect (on_trashcan_contents_altered); 1033 get_offline_bin ().contents_altered.connect (on_offline_contents_altered); 1034 } 1035 1036 protected override MediaSourceHoldingTank create_trashcan () { 1037 return new MediaSourceHoldingTank (this, is_video_trashed, get_video_key); 1038 } 1039 1040 protected override MediaSourceHoldingTank create_offline_bin () { 1041 return new MediaSourceHoldingTank (this, is_video_offline, get_video_key); 1042 } 1043 1044 public override MediaMonitor create_media_monitor (Workers workers, Cancellable cancellable) { 1045 return new VideoMonitor (cancellable); 1046 } 1047 1048 public override bool holds_type_of_source (DataSource source) { 1049 return source is Video; 1050 } 1051 1052 public override string get_typename () { 1053 return Video.TYPENAME; 1054 } 1055 1056 public override bool is_file_recognized (File file) { 1057 return VideoReader.is_supported_video_file (file); 1058 } 1059 1060 private void on_trashcan_contents_altered (Gee.Collection<DataSource>? added, 1061 Gee.Collection<DataSource>? removed) { 1062 trashcan_contents_altered ((Gee.Collection<Video>? ) added, 1063 (Gee.Collection<Video>? ) removed); 1064 } 1065 1066 private void on_offline_contents_altered (Gee.Collection<DataSource>? added, 1067 Gee.Collection<DataSource>? removed) { 1068 offline_contents_altered ((Gee.Collection<Video>? ) added, 1069 (Gee.Collection<Video>? ) removed); 1070 } 1071 1072 protected override MediaSource? fetch_by_numeric_id (int64 numeric_id) { 1073 return fetch (VideoID (numeric_id)); 1074 } 1075 1076 public static int64 get_video_key (DataSource source) { 1077 Video video = (Video) source; 1078 VideoID video_id = video.get_video_id (); 1079 1080 return video_id.id; 1081 } 1082 1083 public static bool is_video_trashed (DataSource source) { 1084 return ((Video) source).is_trashed (); 1085 } 1086 1087 public static bool is_video_offline (DataSource source) { 1088 return ((Video) source).is_offline (); 1089 } 1090 1091 public Video fetch (VideoID video_id) { 1092 return (Video) fetch_by_key (video_id.id); 1093 } 1094 1095 public override Gee.Collection<string> get_event_source_ids (EventID event_id) { 1096 return VideoTable.get_instance ().get_event_source_ids (event_id); 1097 } 1098 1099 public Video? get_state_by_file (File file, out State state) { 1100 Video? video = (Video? ) fetch_by_master_file (file); 1101 if (video != null) { 1102 state = State.ONLINE; 1103 1104 return video; 1105 } 1106 1107 video = (Video? ) get_trashcan ().fetch_by_master_file (file); 1108 if (video != null) { 1109 state = State.TRASH; 1110 1111 return video; 1112 } 1113 1114 video = (Video? ) get_offline_bin ().fetch_by_master_file (file); 1115 if (video != null) { 1116 state = State.OFFLINE; 1117 1118 return video; 1119 } 1120 1121 state = State.UNKNOWN; 1122 1123 return null; 1124 } 1125 1126 private void compare_backing (Video video, FileInfo info, Gee.Collection<Video> matching_master) { 1127 if (video.get_filesize () != info.get_size ()) 1128 return; 1129 1130 if (video.get_timestamp () == info.get_modification_time ().tv_sec) 1131 matching_master.add (video); 1132 } 1133 1134 public void fetch_by_matching_backing (FileInfo info, Gee.Collection<Video> matching_master) { 1135 foreach (DataObject object in get_all ()) 1136 compare_backing ((Video) object, info, matching_master); 1137 1138 foreach (MediaSource media in get_offline_bin_contents ()) 1139 compare_backing ((Video) media, info, matching_master); 1140 } 1141 1142 protected override void notify_contents_altered (Gee.Iterable<DataObject>? added, 1143 Gee.Iterable<DataObject>? removed) { 1144 if (added != null) { 1145 foreach (DataObject object in added) { 1146 Video video = (Video) object; 1147 1148 filesize_to_video.set (video.get_master_filesize (), video); 1149 } 1150 } 1151 1152 if (removed != null) { 1153 foreach (DataObject object in removed) { 1154 Video video = (Video) object; 1155 1156 filesize_to_video.remove (video.get_master_filesize (), video); 1157 } 1158 } 1159 1160 base.notify_contents_altered (added, removed); 1161 } 1162 1163 public VideoID get_basename_filesize_duplicate (string basename, uint64 filesize) { 1164 foreach (Video video in filesize_to_video.get (filesize)) { 1165 if (utf8_ci_compare (video.get_master_file ().get_basename (), basename) == 0) 1166 return video.get_video_id (); 1167 } 1168 1169 return VideoID (); // the default constructor of the VideoID struct creates an invalid 1170 // video id, which is just what we want in this case 1171 } 1172 1173 public bool has_basename_filesize_duplicate (string basename, uint64 filesize) { 1174 return get_basename_filesize_duplicate (basename, filesize).is_valid (); 1175 } 1176} 1177