diff options
author | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2014-07-23 09:06:59 +0200 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2014-07-23 09:06:59 +0200 |
commit | 4ea2cc3bd4a7d9b1c54a9d33e6a1cf82e7c8c21d (patch) | |
tree | d2e54377d14d604356c86862a326f64ae64dadd6 /src/VideoSupport.vala |
Imported Upstream version 0.18.1upstream/0.18.1
Diffstat (limited to 'src/VideoSupport.vala')
-rw-r--r-- | src/VideoSupport.vala | 1188 |
1 files changed, 1188 insertions, 0 deletions
diff --git a/src/VideoSupport.vala b/src/VideoSupport.vala new file mode 100644 index 0000000..e23ba37 --- /dev/null +++ b/src/VideoSupport.vala @@ -0,0 +1,1188 @@ +/* Copyright 2010-2014 Yorba Foundation + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +public errordomain VideoError { + FILE, // there's a problem reading the video container file (doesn't exist, no read + // permission, etc.) + + CONTENTS, // we can read the container file but its contents are indecipherable (no codec, + // malformed data, etc.) +} + +public class VideoImportParams { + // IN: + public File file; + public ImportID import_id = ImportID(); + public string? md5; + public time_t exposure_time_override; + + // IN/OUT: + public Thumbnails? thumbnails; + + // OUT: + public VideoRow row = new VideoRow(); + + public VideoImportParams(File file, ImportID import_id, string? md5, + Thumbnails? thumbnails = null, time_t exposure_time_override = 0) { + this.file = file; + this.import_id = import_id; + this.md5 = md5; + this.thumbnails = thumbnails; + this.exposure_time_override = exposure_time_override; + } +} + +public class VideoReader { + private const double UNKNOWN_CLIP_DURATION = -1.0; + private const uint THUMBNAILER_TIMEOUT = 10000; // In milliseconds. + + // File extensions for video containers that pack only metadata as per the AVCHD spec + private const string[] METADATA_ONLY_FILE_EXTENSIONS = { "bdm", "bdmv", "cpi", "mpl" }; + + private double clip_duration = UNKNOWN_CLIP_DURATION; + private Gdk.Pixbuf preview_frame = null; + private File file = null; + private GLib.Pid thumbnailer_pid = 0; + public DateTime? timestamp { get; private set; default = null; } + + public VideoReader(File file) { + this.file = file; + } + + public static bool is_supported_video_file(File file) { + return is_supported_video_filename(file.get_basename()); + } + + public static bool is_supported_video_filename(string filename) { + string mime_type; + mime_type = ContentType.guess(filename, new uchar[0], null); + if (mime_type.length >= 6 && mime_type[0:6] == "video/") { + string? extension = null; + string? name = null; + disassemble_filename(filename, out name, out extension); + + if (extension == null) + return true; + + foreach (string s in METADATA_ONLY_FILE_EXTENSIONS) { + if (utf8_ci_compare(s, extension) == 0) + return false; + } + + return true; + } else { + return false; + } + } + + public static ImportResult prepare_for_import(VideoImportParams params) { +#if MEASURE_IMPORT + Timer total_time = new Timer(); +#endif + File file = params.file; + + FileInfo info = null; + try { + info = file.query_info(DirectoryMonitor.SUPPLIED_ATTRIBUTES, + FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null); + } catch (Error err) { + return ImportResult.FILE_ERROR; + } + + if (info.get_file_type() != FileType.REGULAR) + return ImportResult.NOT_A_FILE; + + if (!is_supported_video_file(file)) { + message("Not importing %s: file is marked as a video file but doesn't have a" + + "supported extension", file.get_path()); + + return ImportResult.UNSUPPORTED_FORMAT; + } + + TimeVal timestamp = info.get_modification_time(); + + // make sure params has a valid md5 + assert(params.md5 != null); + + time_t exposure_time = params.exposure_time_override; + string title = ""; + string comment = ""; + + VideoReader reader = new VideoReader(file); + bool is_interpretable = true; + double clip_duration = 0.0; + Gdk.Pixbuf preview_frame = reader.read_preview_frame(); + try { + clip_duration = reader.read_clip_duration(); + } catch (VideoError err) { + if (err is VideoError.FILE) { + return ImportResult.FILE_ERROR; + } else if (err is VideoError.CONTENTS) { + is_interpretable = false; + clip_duration = 0.0; + } else { + error("can't prepare video for import: an unknown kind of video error occurred"); + } + } + + try { + VideoMetadata metadata = reader.read_metadata(); + MetadataDateTime? creation_date_time = metadata.get_creation_date_time(); + + if (creation_date_time != null && creation_date_time.get_timestamp() != 0) + exposure_time = creation_date_time.get_timestamp(); + + string? video_title = metadata.get_title(); + string? video_comment = metadata.get_comment(); + if (video_title != null) + title = video_title; + if (video_comment != null) + comment = video_comment; + } catch (Error err) { + warning("Unable to read video metadata: %s", err.message); + } + + if (exposure_time == 0) { + // Use time reported by Gstreamer, if available. + exposure_time = (time_t) (reader.timestamp != null ? + reader.timestamp.to_unix() : 0); + } + + params.row.video_id = VideoID(); + params.row.filepath = file.get_path(); + params.row.filesize = info.get_size(); + params.row.timestamp = timestamp.tv_sec; + params.row.width = preview_frame.width; + params.row.height = preview_frame.height; + params.row.clip_duration = clip_duration; + params.row.is_interpretable = is_interpretable; + params.row.exposure_time = exposure_time; + params.row.import_id = params.import_id; + params.row.event_id = EventID(); + params.row.md5 = params.md5; + params.row.time_created = 0; + params.row.title = title; + params.row.comment = comment; + params.row.backlinks = ""; + params.row.time_reimported = 0; + params.row.flags = 0; + + if (params.thumbnails != null) { + params.thumbnails = new Thumbnails(); + ThumbnailCache.generate_for_video_frame(params.thumbnails, preview_frame); + } + +#if MEASURE_IMPORT + debug("IMPORT: total time to import video = %lf", total_time.elapsed()); +#endif + return ImportResult.SUCCESS; + } + + private void read_internal() throws VideoError { + if (!does_file_exist()) + throw new VideoError.FILE("video file '%s' does not exist or is inaccessible".printf( + file.get_path())); + + try { + Gst.PbUtils.Discoverer d = new Gst.PbUtils.Discoverer((Gst.ClockTime) (Gst.SECOND * 5)); + Gst.PbUtils.DiscovererInfo info = d.discover_uri(file.get_uri()); + + clip_duration = ((double) info.get_duration()) / 1000000000.0; + + // Get creation time. + // TODO: Note that TAG_DATE can be changed to TAG_DATE_TIME in the future + // (and the corresponding output struct) in order to implement #2836. + Date? video_date = null; + if (info.get_tags() != null && info.get_tags().get_date(Gst.Tags.DATE, out video_date)) { + timestamp = new DateTime.local(video_date.get_year(), video_date.get_month(), + video_date.get_day(), 0, 0, 0); + } + } catch (Error e) { + debug("Video read error: %s", e.message); + throw new VideoError.CONTENTS("GStreamer couldn't extract clip information: %s" + .printf(e.message)); + } + } + + // Used by thumbnailer() to kill the external process if need be. + private bool on_thumbnailer_timer() { + debug("Thumbnailer timer called"); + if (thumbnailer_pid != 0) { + debug("Killing thumbnailer process: %d", thumbnailer_pid); + Posix.kill(thumbnailer_pid, Posix.SIGKILL); + } + return false; // Don't call again. + } + + // Performs video thumbnailing. + // Note: not thread-safe if called from the same instance of the class. + private Gdk.Pixbuf? thumbnailer(string video_file) { + // Use Shotwell's thumbnailer, redirect output to stdout. + debug("Launching thumbnailer process: %s", AppDirs.get_thumbnailer_bin().get_path()); + string[] argv = {AppDirs.get_thumbnailer_bin().get_path(), video_file}; + int child_stdout; + try { + GLib.Process.spawn_async_with_pipes(null, argv, null, GLib.SpawnFlags.SEARCH_PATH | + GLib.SpawnFlags.DO_NOT_REAP_CHILD, null, out thumbnailer_pid, null, out child_stdout, + null); + debug("Spawned thumbnailer, child pid: %d", (int) thumbnailer_pid); + } catch (Error e) { + debug("Error spawning process: %s", e.message); + if (thumbnailer_pid != 0) + GLib.Process.close_pid(thumbnailer_pid); + return null; + } + + // Start timer. + Timeout.add(THUMBNAILER_TIMEOUT, on_thumbnailer_timer); + + // Read pixbuf from stream. + Gdk.Pixbuf? buf = null; + try { + GLib.UnixInputStream unix_input = new GLib.UnixInputStream(child_stdout, true); + buf = new Gdk.Pixbuf.from_stream(unix_input, null); + } catch (Error e) { + debug("Error creating pixbuf: %s", e.message); + buf = null; + } + + // Make sure process exited properly. + int child_status = 0; + int ret_waitpid = Posix.waitpid(thumbnailer_pid, out child_status, 0); + if (ret_waitpid < 0) { + debug("waitpid returned error code: %d", ret_waitpid); + buf = null; + } else if (0 != posix_wexitstatus(child_status)) { + debug("Thumbnailer exited with error code: %d", posix_wexitstatus(child_status)); + buf = null; + } + + Posix.close(child_stdout); + GLib.Process.close_pid(thumbnailer_pid); + thumbnailer_pid = 0; + return buf; + } + + private bool does_file_exist() { + return FileUtils.test(file.get_path(), FileTest.EXISTS | FileTest.IS_REGULAR); + } + + public Gdk.Pixbuf? read_preview_frame() { + if (preview_frame != null) + return preview_frame; + + if (!does_file_exist()) + return null; + + // Get preview frame from thumbnailer. + preview_frame = thumbnailer(file.get_path()); + if (null == preview_frame) + preview_frame = Resources.get_noninterpretable_badge_pixbuf(); + + return preview_frame; + } + + public double read_clip_duration() throws VideoError { + if (clip_duration == UNKNOWN_CLIP_DURATION) + read_internal(); + + return clip_duration; + } + + public VideoMetadata read_metadata() throws Error { + VideoMetadata metadata = new VideoMetadata(); + metadata.read_from_file(File.new_for_path(file.get_path())); + + return metadata; + } +} + +public class Video : VideoSource, Flaggable, Monitorable, Dateable { + public const string TYPENAME = "video"; + + public const uint64 FLAG_TRASH = 0x0000000000000001; + public const uint64 FLAG_OFFLINE = 0x0000000000000002; + public const uint64 FLAG_FLAGGED = 0x0000000000000004; + + public class InterpretableResults { + internal Video video; + internal bool update_interpretable = false; + internal bool is_interpretable = false; + internal Gdk.Pixbuf? new_thumbnail = null; + + public InterpretableResults(Video video) { + this.video = video; + } + + public void foreground_finish() { + if (update_interpretable) + video.set_is_interpretable(is_interpretable); + + if (new_thumbnail != null) { + try { + ThumbnailCache.replace(video, ThumbnailCache.Size.BIG, new_thumbnail); + ThumbnailCache.replace(video, ThumbnailCache.Size.MEDIUM, new_thumbnail); + + video.notify_thumbnail_altered(); + } catch (Error err) { + message("Unable to update video thumbnails for %s: %s", video.to_string(), + err.message); + } + } + } + } + + private static bool interpreter_state_changed; + private static int current_state; + private static bool normal_regen_complete; + private static bool offline_regen_complete; + public static VideoSourceCollection global; + + private VideoRow backing_row; + + public Video(VideoRow row) { + this.backing_row = row; + + // normalize user text + this.backing_row.title = prep_title(this.backing_row.title); + + if (((row.flags & FLAG_TRASH) != 0) || ((row.flags & FLAG_OFFLINE) != 0)) + rehydrate_backlinks(global, row.backlinks); + } + + public static void init(ProgressMonitor? monitor = null) { + // Must initialize static variables here. + // TODO: set values at declaration time once the following Vala bug is fixed: + // https://bugzilla.gnome.org/show_bug.cgi?id=655594 + interpreter_state_changed = false; + current_state = -1; + normal_regen_complete = false; + offline_regen_complete = false; + + // initialize GStreamer, but don't pass it our actual command line arguments -- we don't + // want our end users to be able to parameterize the GStreamer configuration + string[] fake_args = new string[0]; + unowned string[] fake_unowned_args = fake_args; + Gst.init(ref fake_unowned_args); + + int saved_state = Config.Facade.get_instance().get_video_interpreter_state_cookie(); + current_state = (int) Gst.Registry.get().get_feature_list_cookie(); + if (saved_state == Config.Facade.NO_VIDEO_INTERPRETER_STATE) { + message("interpreter state cookie not found; assuming all video thumbnails are out of date"); + interpreter_state_changed = true; + } else if (saved_state != current_state) { + message("interpreter state has changed; video thumbnails may be out of date"); + interpreter_state_changed = true; + } + + global = new VideoSourceCollection(); + + Gee.ArrayList<VideoRow?> all = VideoTable.get_instance().get_all(); + Gee.ArrayList<Video> all_videos = new Gee.ArrayList<Video>(); + Gee.ArrayList<Video> trashed_videos = new Gee.ArrayList<Video>(); + Gee.ArrayList<Video> offline_videos = new Gee.ArrayList<Video>(); + int count = all.size; + for (int ctr = 0; ctr < count; ctr++) { + Video video = new Video(all.get(ctr)); + + if (interpreter_state_changed) + video.set_is_interpretable(false); + + if (video.is_trashed()) + trashed_videos.add(video); + else if (video.is_offline()) + offline_videos.add(video); + else + all_videos.add(video); + + if (monitor != null) + monitor(ctr, count); + } + + global.add_many_to_trash(trashed_videos); + global.add_many_to_offline(offline_videos); + global.add_many(all_videos); + } + + public static bool has_interpreter_state_changed() { + return interpreter_state_changed; + } + + public static void notify_normal_thumbs_regenerated() { + if (normal_regen_complete) + return; + + message("normal video thumbnail regeneration completed"); + + normal_regen_complete = true; + if (normal_regen_complete && offline_regen_complete) + save_interpreter_state(); + } + + public static void notify_offline_thumbs_regenerated() { + if (offline_regen_complete) + return; + + message("offline video thumbnail regeneration completed"); + + offline_regen_complete = true; + if (normal_regen_complete && offline_regen_complete) + save_interpreter_state(); + } + + private static void save_interpreter_state() { + if (interpreter_state_changed) { + message("saving video interpreter state to configuration system"); + + Config.Facade.get_instance().set_video_interpreter_state_cookie(current_state); + interpreter_state_changed = false; + } + } + + public static void terminate() { + } + + public static ExporterUI? export_many(Gee.Collection<Video> videos, Exporter.CompletionCallback done, + bool export_in_place = false) { + if (videos.size == 0) + return null; + + // in place export is relatively easy -- provide a fast, separate code path for it + if (export_in_place) { + ExporterUI temp_exporter = new ExporterUI(new Exporter.for_temp_file(videos, + Scaling.for_original(), ExportFormatParameters.unmodified())); + temp_exporter.export(done); + return temp_exporter; + } + + // one video + if (videos.size == 1) { + Video video = null; + foreach (Video v in videos) { + video = v; + break; + } + + File save_as = ExportUI.choose_file(video.get_basename()); + if (save_as == null) + return null; + + try { + AppWindow.get_instance().set_busy_cursor(); + video.export(save_as); + AppWindow.get_instance().set_normal_cursor(); + } catch (Error err) { + AppWindow.get_instance().set_normal_cursor(); + export_error_dialog(save_as, false); + } + + return null; + } + + // multiple videos + File export_dir = ExportUI.choose_dir(_("Export Videos")); + if (export_dir == null) + return null; + + ExporterUI exporter = new ExporterUI(new Exporter(videos, export_dir, + Scaling.for_original(), ExportFormatParameters.unmodified())); + exporter.export(done); + + return exporter; + } + + protected override void commit_backlinks(SourceCollection? sources, string? backlinks) { + try { + VideoTable.get_instance().update_backlinks(get_video_id(), backlinks); + lock (backing_row) { + backing_row.backlinks = backlinks; + } + } catch (DatabaseError err) { + warning("Unable to update link state for %s: %s", to_string(), err.message); + } + } + + protected override bool set_event_id(EventID event_id) { + lock (backing_row) { + bool committed = VideoTable.get_instance().set_event(backing_row.video_id, event_id); + + if (committed) + backing_row.event_id = event_id; + + return committed; + } + } + + public static bool is_duplicate(File? file, string? full_md5) { + assert(file != null || full_md5 != null); +#if !NO_DUPE_DETECTION + return VideoTable.get_instance().has_duplicate(file, full_md5); +#else + return false; +#endif + } + + public static ImportResult import_create(VideoImportParams params, out Video video) { + video = null; + + // add to the database + try { + if (VideoTable.get_instance().add(params.row).is_invalid()) + return ImportResult.DATABASE_ERROR; + } catch (DatabaseError err) { + return ImportResult.DATABASE_ERROR; + } + + // create local object but don't add to global until thumbnails generated + video = new Video(params.row); + + return ImportResult.SUCCESS; + } + + public static void import_failed(Video video) { + try { + VideoTable.get_instance().remove(video.get_video_id()); + } catch (DatabaseError err) { + AppWindow.database_error(err); + } + } + + public override BackingFileState[] get_backing_files_state() { + BackingFileState[] backing = new BackingFileState[1]; + lock (backing_row) { + backing[0] = new BackingFileState(backing_row.filepath, backing_row.filesize, + backing_row.timestamp, backing_row.md5); + } + + return backing; + } + + public override Gdk.Pixbuf? get_thumbnail(int scale) throws Error { + return ThumbnailCache.fetch(this, scale); + } + + public override string get_master_md5() { + lock (backing_row) { + return backing_row.md5; + } + } + + public override Gdk.Pixbuf get_preview_pixbuf(Scaling scaling) throws Error { + Gdk.Pixbuf pixbuf = get_thumbnail(ThumbnailCache.Size.BIG); + + return scaling.perform_on_pixbuf(pixbuf, Gdk.InterpType.NEAREST, true); + } + + public override Gdk.Pixbuf? create_thumbnail(int scale) throws Error { + VideoReader reader = new VideoReader(get_file()); + Gdk.Pixbuf? frame = reader.read_preview_frame(); + + return (frame != null) ? frame : Resources.get_noninterpretable_badge_pixbuf().copy(); + } + + public override string get_typename() { + return TYPENAME; + } + + public override int64 get_instance_id() { + return get_video_id().id; + } + + public override ImportID get_import_id() { + lock (backing_row) { + return backing_row.import_id; + } + } + + public override PhotoFileFormat get_preferred_thumbnail_format() { + return PhotoFileFormat.get_system_default_format(); + } + + public override string? get_title() { + lock (backing_row) { + return backing_row.title; + } + } + + public override void set_title(string? title) { + string? new_title = prep_title(title); + + lock (backing_row) { + if (backing_row.title == new_title) + return; + + try { + VideoTable.get_instance().set_title(backing_row.video_id, new_title); + } catch (DatabaseError e) { + AppWindow.database_error(e); + return; + } + // if we didn't short-circuit return in the catch clause above, then the change was + // successfully committed to the database, so update it in the in-memory row cache + backing_row.title = new_title; + } + + notify_altered(new Alteration("metadata", "name")); + } + + public override string? get_comment() { + lock (backing_row) { + return backing_row.comment; + } + } + + public override bool set_comment(string? comment) { + string? new_comment = prep_title(comment); + + lock (backing_row) { + if (backing_row.comment == new_comment) + return true; + + try { + VideoTable.get_instance().set_comment(backing_row.video_id, new_comment); + } catch (DatabaseError e) { + AppWindow.database_error(e); + return false; + } + // if we didn't short-circuit return in the catch clause above, then the change was + // successfully committed to the database, so update it in the in-memory row cache + backing_row.comment = new_comment; + } + + notify_altered(new Alteration("metadata", "comment")); + + return true; + } + + + public override Rating get_rating() { + lock (backing_row) { + return backing_row.rating; + } + } + + public override void set_rating(Rating rating) { + lock (backing_row) { + if ((!rating.is_valid()) || (rating == backing_row.rating)) + return; + + try { + VideoTable.get_instance().set_rating(get_video_id(), rating); + } catch (DatabaseError e) { + AppWindow.database_error(e); + return; + } + // if we didn't short-circuit return in the catch clause above, then the change was + // successfully committed to the database, so update it in the in-memory row cache + backing_row.rating = rating; + } + + notify_altered(new Alteration("metadata", "rating")); + } + + public override void increase_rating() { + lock (backing_row) { + set_rating(backing_row.rating.increase()); + } + } + + public override void decrease_rating() { + lock (backing_row) { + set_rating(backing_row.rating.decrease()); + } + } + + public override bool is_trashed() { + return is_flag_set(FLAG_TRASH); + } + + public override bool is_offline() { + return is_flag_set(FLAG_OFFLINE); + } + + public override void mark_offline() { + add_flags(FLAG_OFFLINE); + } + + public override void mark_online() { + remove_flags(FLAG_OFFLINE); + + if ((!get_is_interpretable()) && has_interpreter_state_changed()) + check_is_interpretable().foreground_finish(); + } + + public override void trash() { + add_flags(FLAG_TRASH); + } + + public override void untrash() { + remove_flags(FLAG_TRASH); + } + + public bool is_flagged() { + return is_flag_set(FLAG_FLAGGED); + } + + public void mark_flagged() { + add_flags(FLAG_FLAGGED, new Alteration("metadata", "flagged")); + } + + public void mark_unflagged() { + remove_flags(FLAG_FLAGGED, new Alteration("metadata", "flagged")); + } + + public override EventID get_event_id() { + lock (backing_row) { + return backing_row.event_id; + } + } + + public override string to_string() { + lock (backing_row) { + return "[%s] %s".printf(backing_row.video_id.id.to_string(), backing_row.filepath); + } + } + + public VideoID get_video_id() { + lock (backing_row) { + return backing_row.video_id; + } + } + + public override time_t get_exposure_time() { + lock (backing_row) { + return backing_row.exposure_time; + } + } + + public void set_exposure_time(time_t time) { + lock (backing_row) { + try { + VideoTable.get_instance().set_exposure_time(backing_row.video_id, time); + } catch (Error e) { + debug("Warning - %s", e.message); + } + backing_row.exposure_time = time; + } + + notify_altered(new Alteration("metadata", "exposure-time")); + } + + public Dimensions get_frame_dimensions() { + lock (backing_row) { + return Dimensions(backing_row.width, backing_row.height); + } + } + + public override Dimensions get_dimensions(Photo.Exception disallowed_steps = Photo.Exception.NONE) { + return get_frame_dimensions(); + } + + public override uint64 get_filesize() { + return get_master_filesize(); + } + + public override uint64 get_master_filesize() { + lock (backing_row) { + return backing_row.filesize; + } + } + + public override time_t get_timestamp() { + lock (backing_row) { + return backing_row.timestamp; + } + } + + public void set_master_timestamp(FileInfo info) { + TimeVal time_val = info.get_modification_time(); + + try { + lock (backing_row) { + if (backing_row.timestamp == time_val.tv_sec) + return; + + VideoTable.get_instance().set_timestamp(backing_row.video_id, time_val.tv_sec); + backing_row.timestamp = time_val.tv_sec; + } + } catch (DatabaseError err) { + AppWindow.database_error(err); + + return; + } + + notify_altered(new Alteration("metadata", "master-timestamp")); + } + + public string get_filename() { + lock (backing_row) { + return backing_row.filepath; + } + } + + public override File get_file() { + return File.new_for_path(get_filename()); + } + + public override File get_master_file() { + return get_file(); + } + + public void export(File dest_file) throws Error { + File source_file = File.new_for_path(get_filename()); + source_file.copy(dest_file, FileCopyFlags.OVERWRITE | FileCopyFlags.TARGET_DEFAULT_PERMS, + null, null); + } + + public double get_clip_duration() { + lock (backing_row) { + return backing_row.clip_duration; + } + } + + public bool get_is_interpretable() { + lock (backing_row) { + return backing_row.is_interpretable; + } + } + + private void set_is_interpretable(bool is_interpretable) { + lock (backing_row) { + if (backing_row.is_interpretable == is_interpretable) + return; + + backing_row.is_interpretable = is_interpretable; + } + + try { + VideoTable.get_instance().update_is_interpretable(get_video_id(), is_interpretable); + } catch (DatabaseError e) { + AppWindow.database_error(e); + } + } + + // Intended to be called from a background thread but can be called from foreground as well. + // Caller should call InterpretableResults.foreground_process() only from foreground thread, + // however + public InterpretableResults check_is_interpretable() { + InterpretableResults results = new InterpretableResults(this); + + double clip_duration = -1.0; + Gdk.Pixbuf? preview_frame = null; + + VideoReader backing_file_reader = new VideoReader(get_file()); + try { + clip_duration = backing_file_reader.read_clip_duration(); + preview_frame = backing_file_reader.read_preview_frame(); + } catch (VideoError e) { + // if we catch an error on an interpretable video here, then this video is + // non-interpretable (e.g. its codec is not present on the users system). + results.update_interpretable = get_is_interpretable(); + results.is_interpretable = false; + + return results; + } + + // if already marked interpretable, this is only confirming what we already knew + if (get_is_interpretable()) { + results.update_interpretable = false; + results.is_interpretable = true; + + return results; + } + + debug("video %s has become interpretable", get_file().get_basename()); + + // save this here, this can be done in background thread + lock (backing_row) { + backing_row.clip_duration = clip_duration; + } + + results.update_interpretable = true; + results.is_interpretable = true; + results.new_thumbnail = preview_frame; + + return results; + } + + public override void destroy() { + VideoID video_id = get_video_id(); + + ThumbnailCache.remove(this); + + try { + VideoTable.get_instance().remove(video_id); + } catch (DatabaseError err) { + error("failed to remove video %s from video table", to_string()); + } + + base.destroy(); + } + + protected override bool internal_delete_backing() throws Error { + bool ret = delete_original_file(); + + // Return false if parent method failed. + return base.internal_delete_backing() && ret; + } + + private void notify_flags_altered(Alteration? additional_alteration) { + Alteration alteration = new Alteration("metadata", "flags"); + if (additional_alteration != null) + alteration = alteration.compress(additional_alteration); + + notify_altered(alteration); + } + + public uint64 add_flags(uint64 flags_to_add, Alteration? additional_alteration = null) { + uint64 new_flags; + lock (backing_row) { + new_flags = internal_add_flags(backing_row.flags, flags_to_add); + if (backing_row.flags == new_flags) + return backing_row.flags; + + try { + VideoTable.get_instance().set_flags(get_video_id(), new_flags); + } catch (DatabaseError e) { + AppWindow.database_error(e); + return backing_row.flags; + } + + backing_row.flags = new_flags; + } + + notify_flags_altered(additional_alteration); + + return new_flags; + } + + public uint64 remove_flags(uint64 flags_to_remove, Alteration? additional_alteration = null) { + uint64 new_flags; + lock (backing_row) { + new_flags = internal_remove_flags(backing_row.flags, flags_to_remove); + if (backing_row.flags == new_flags) + return backing_row.flags; + + try { + VideoTable.get_instance().set_flags(get_video_id(), new_flags); + } catch (DatabaseError e) { + AppWindow.database_error(e); + return backing_row.flags; + } + + backing_row.flags = new_flags; + } + + notify_flags_altered(additional_alteration); + + return new_flags; + } + + public bool is_flag_set(uint64 flag) { + lock (backing_row) { + return internal_is_flag_set(backing_row.flags, flag); + } + } + + public void set_master_file(File file) { + string new_filepath = file.get_path(); + string? old_filepath = null; + try { + lock (backing_row) { + if (backing_row.filepath == new_filepath) + return; + + old_filepath = backing_row.filepath; + + VideoTable.get_instance().set_filepath(backing_row.video_id, new_filepath); + backing_row.filepath = new_filepath; + } + } catch (DatabaseError err) { + AppWindow.database_error(err); + + return; + } + + assert(old_filepath != null); + notify_master_replaced(File.new_for_path(old_filepath), file); + + notify_altered(new Alteration.from_list("backing:master,metadata:name")); + } + + public VideoMetadata read_metadata() throws Error { + return (new VideoReader(get_file())).read_metadata(); + } +} + +public class VideoSourceCollection : MediaSourceCollection { + public enum State { + UNKNOWN, + ONLINE, + OFFLINE, + TRASH + } + + public override TransactionController transaction_controller { + get { + if (_transaction_controller == null) + _transaction_controller = new MediaSourceTransactionController(this); + + return _transaction_controller; + } + } + + private TransactionController _transaction_controller = null; + private Gee.MultiMap<uint64?, Video> filesize_to_video = + new Gee.TreeMultiMap<uint64?, Video>(uint64_compare); + + public VideoSourceCollection() { + base("VideoSourceCollection", get_video_key); + + get_trashcan().contents_altered.connect(on_trashcan_contents_altered); + get_offline_bin().contents_altered.connect(on_offline_contents_altered); + } + + protected override MediaSourceHoldingTank create_trashcan() { + return new MediaSourceHoldingTank(this, is_video_trashed, get_video_key); + } + + protected override MediaSourceHoldingTank create_offline_bin() { + return new MediaSourceHoldingTank(this, is_video_offline, get_video_key); + } + + public override MediaMonitor create_media_monitor(Workers workers, Cancellable cancellable) { + return new VideoMonitor(cancellable); + } + + public override bool holds_type_of_source(DataSource source) { + return source is Video; + } + + public override string get_typename() { + return Video.TYPENAME; + } + + public override bool is_file_recognized(File file) { + return VideoReader.is_supported_video_file(file); + } + + private void on_trashcan_contents_altered(Gee.Collection<DataSource>? added, + Gee.Collection<DataSource>? removed) { + trashcan_contents_altered((Gee.Collection<Video>?) added, + (Gee.Collection<Video>?) removed); + } + + private void on_offline_contents_altered(Gee.Collection<DataSource>? added, + Gee.Collection<DataSource>? removed) { + offline_contents_altered((Gee.Collection<Video>?) added, + (Gee.Collection<Video>?) removed); + } + + protected override MediaSource? fetch_by_numeric_id(int64 numeric_id) { + return fetch(VideoID(numeric_id)); + } + + public static int64 get_video_key(DataSource source) { + Video video = (Video) source; + VideoID video_id = video.get_video_id(); + + return video_id.id; + } + + public static bool is_video_trashed(DataSource source) { + return ((Video) source).is_trashed(); + } + + public static bool is_video_offline(DataSource source) { + return ((Video) source).is_offline(); + } + + public Video fetch(VideoID video_id) { + return (Video) fetch_by_key(video_id.id); + } + + public override Gee.Collection<string> get_event_source_ids(EventID event_id){ + return VideoTable.get_instance().get_event_source_ids(event_id); + } + + public Video? get_state_by_file(File file, out State state) { + Video? video = (Video?) fetch_by_master_file(file); + if (video != null) { + state = State.ONLINE; + + return video; + } + + video = (Video?) get_trashcan().fetch_by_master_file(file); + if (video != null) { + state = State.TRASH; + + return video; + } + + video = (Video?) get_offline_bin().fetch_by_master_file(file); + if (video != null) { + state = State.OFFLINE; + + return video; + } + + state = State.UNKNOWN; + + return null; + } + + private void compare_backing(Video video, FileInfo info, Gee.Collection<Video> matching_master) { + if (video.get_filesize() != info.get_size()) + return; + + if (video.get_timestamp() == info.get_modification_time().tv_sec) + matching_master.add(video); + } + + public void fetch_by_matching_backing(FileInfo info, Gee.Collection<Video> matching_master) { + foreach (DataObject object in get_all()) + compare_backing((Video) object, info, matching_master); + + foreach (MediaSource media in get_offline_bin_contents()) + compare_backing((Video) media, info, matching_master); + } + + protected override void notify_contents_altered(Gee.Iterable<DataObject>? added, + Gee.Iterable<DataObject>? removed) { + if (added != null) { + foreach (DataObject object in added) { + Video video = (Video) object; + + filesize_to_video.set(video.get_master_filesize(), video); + } + } + + if (removed != null) { + foreach (DataObject object in removed) { + Video video = (Video) object; + + filesize_to_video.remove(video.get_master_filesize(), video); + } + } + + base.notify_contents_altered(added, removed); + } + + public VideoID get_basename_filesize_duplicate(string basename, uint64 filesize) { + foreach (Video video in filesize_to_video.get(filesize)) { + if (utf8_ci_compare(video.get_master_file().get_basename(), basename) == 0) + return video.get_video_id(); + } + + return VideoID(); // the default constructor of the VideoID struct creates an invalid + // video id, which is just what we want in this case + } + + public bool has_basename_filesize_duplicate(string basename, uint64 filesize) { + return get_basename_filesize_duplicate(basename, filesize).is_valid(); + } +} |