[shotwell/wip/phako/128: 1/4] video-support: Move into subdirectory




commit 5e3de8bcec3b4ffbeda73c437ef4219b48c24682
Author: Jens Georg <mail jensge org>
Date:   Tue May 7 21:17:50 2019 +0200

    video-support: Move into subdirectory
    
    And split VideoSupport.vala into classes

 src/meson.build                                    | 475 +++++++-------
 .../Video.vala}                                    | 687 ++++-----------------
 src/video-support/VideoImportParams.vala           |  28 +
 src/video-support/VideoReader.vala                 | 302 +++++++++
 src/video-support/VideoSourceCollection.vala       | 175 ++++++
 5 files changed, 853 insertions(+), 814 deletions(-)
---
diff --git a/src/meson.build b/src/meson.build
index 304d095c..285124b9 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -42,238 +42,247 @@ shotwell_deps = [gio, gee, sqlite, gtk, sqlite, posix, gphoto2,
 
 subdir('publishing')
 
-executable('shotwell',
-           ['unit/Unit.vala',
-            'util/Util.vala',
-            'util/file.vala',
-            'util/image.vala',
-            'util/misc.vala',
-            'util/string.vala',
-            'util/system.vala',
-            'util/ui.vala',
-            'threads/Threads.vala',
-            'threads/Workers.vala',
-            'threads/BackgroundJob.vala',
-            'threads/Semaphore.vala',
-            'db/Db.vala',
-            'db/DatabaseTable.vala',
-            'db/PhotoTable.vala',
-            'db/EventTable.vala',
-            'db/FaceLocationTable.vala',
-            'db/FaceTable.vala',
-            'db/TagTable.vala',
-            'db/TombstoneTable.vala',
-            'db/VideoTable.vala',
-            'db/VersionTable.vala',
-            'db/SavedSearchDBTable.vala',
-            'editing_tools/EditingTools.vala',
-            'editing_tools/RGBHistogramManipulator.vala',
-            'editing_tools/StraightenTool.vala',
-            'faces/Face.vala',
-            'faces/FaceLocation.vala',
-            'slideshow/Slideshow.vala',
-            'slideshow/TransitionEffects.vala',
-            'photos/Photos.vala',
-            'photos/PhotoFileAdapter.vala',
-            'photos/PhotoFileFormat.vala',
-            'photos/PhotoFileSniffer.vala',
-            'photos/PhotoMetadata.vala',
-            'photos/GRaw.vala',
-            'photos/GdkSupport.vala',
-            'photos/GifSupport.vala',
-            'photos/JfifSupport.vala',
-            'photos/BmpSupport.vala',
-            'photos/RawSupport.vala',
-            'photos/PngSupport.vala',
-            'photos/TiffSupport.vala',
-            'photos/WebPSupport.vala',
-            'plugins/Plugins.vala',
-            'plugins/StandardHostInterface.vala',
-            'plugins/ManifestWidget.vala',
-            'publishing/Publishing.vala',
-            'publishing/PublishingUI.vala',
-            'publishing/PublishingPluginHost.vala',
-            'publishing/APIGlue.vala',
+executable(
+    'shotwell',
+    [
+        'unit/Unit.vala',
+        'util/Util.vala',
+        'util/file.vala',
+        'util/image.vala',
+        'util/misc.vala',
+        'util/string.vala',
+        'util/system.vala',
+        'util/ui.vala',
+        'threads/Threads.vala',
+        'threads/Workers.vala',
+        'threads/BackgroundJob.vala',
+        'threads/Semaphore.vala',
+        'db/Db.vala',
+        'db/DatabaseTable.vala',
+        'db/PhotoTable.vala',
+        'db/EventTable.vala',
+        'db/FaceLocationTable.vala',
+        'db/FaceTable.vala',
+        'db/TagTable.vala',
+        'db/TombstoneTable.vala',
+        'db/VideoTable.vala',
+        'db/VersionTable.vala',
+        'db/SavedSearchDBTable.vala',
+        'editing_tools/EditingTools.vala',
+        'editing_tools/RGBHistogramManipulator.vala',
+        'editing_tools/StraightenTool.vala',
+        'faces/Face.vala',
+        'faces/FaceLocation.vala',
+        'slideshow/Slideshow.vala',
+        'slideshow/TransitionEffects.vala',
+        'photos/Photos.vala',
+        'photos/PhotoFileAdapter.vala',
+        'photos/PhotoFileFormat.vala',
+        'photos/PhotoFileSniffer.vala',
+        'photos/PhotoMetadata.vala',
+        'photos/GRaw.vala',
+        'photos/GdkSupport.vala',
+        'photos/GifSupport.vala',
+        'photos/JfifSupport.vala',
+        'photos/BmpSupport.vala',
+        'photos/RawSupport.vala',
+        'photos/PngSupport.vala',
+        'photos/TiffSupport.vala',
+        'photos/WebPSupport.vala',
+        'plugins/Plugins.vala',
+        'plugins/StandardHostInterface.vala',
+        'plugins/ManifestWidget.vala',
+        'publishing/Publishing.vala',
+        'publishing/PublishingUI.vala',
+        'publishing/PublishingPluginHost.vala',
+        'publishing/APIGlue.vala',
             'library/BackgroundProgressBar.vala',
-            'library/Library.vala',
-            'library/LibraryWindow.vala',
-            'library/LibraryBranch.vala',
-            'library/TrashSidebarEntry.vala',
-            'library/OfflineSidebarEntry.vala',
-            'library/FlaggedSidebarEntry.vala',
-            'library/LastImportSidebarEntry.vala',
-            'library/ImportQueueSidebarEntry.vala',
-            'library/FlaggedPage.vala',
-            'library/ImportQueuePage.vala',
-            'library/LastImportPage.vala',
-            'library/OfflinePage.vala',
-            'library/TrashPage.vala',
-            'direct/Direct.vala',
-            'direct/DirectWindow.vala',
-            'direct/DirectPhoto.vala',
-            'direct/DirectPhotoPage.vala',
-            'direct/DirectView.vala',
-            'core/Core.vala',
-            'core/DataCollection.vala',
-            'core/DataSet.vala',
-            'core/util.vala',
-            'core/SourceCollection.vala',
-            'core/SourceHoldingTank.vala',
-            'core/DatabaseSourceCollection.vala',
-            'core/ContainerSourceCollection.vala',
-            'core/ViewCollection.vala',
-            'core/DataObject.vala',
-            'core/Alteration.vala',
-            'core/DataSource.vala',
-            'core/DataSourceTypes.vala',
-            'core/DataView.vala',
-            'core/DataViewTypes.vala',
-            'core/Tracker.vala',
-            'core/SourceInterfaces.vala',
-            'sidebar/Sidebar.vala',
-            'sidebar/Branch.vala',
-            'sidebar/Entry.vala',
-            'sidebar/Tree.vala',
-            'sidebar/common.vala',
-            'events/Events.vala',
-            'events/EventsBranch.vala',
-            'events/EventsDirectoryPage.vala',
-            'events/EventPage.vala',
-            'events/EventDirectoryItem.vala',
-            'tags/Tags.vala',
-            'tags/TagsBranch.vala',
-            'tags/TagPage.vala',
-            'tags/HierarchicalTagIndex.vala',
-            'tags/HierarchicalTagUtilities.vala',
-            'camera/Camera.vala',
-            'camera/CameraBranch.vala',
-            'camera/CameraTable.vala',
+        'library/Library.vala',
+        'library/LibraryWindow.vala',
+        'library/LibraryBranch.vala',
+        'library/TrashSidebarEntry.vala',
+        'library/OfflineSidebarEntry.vala',
+        'library/FlaggedSidebarEntry.vala',
+        'library/LastImportSidebarEntry.vala',
+        'library/ImportQueueSidebarEntry.vala',
+        'library/FlaggedPage.vala',
+        'library/ImportQueuePage.vala',
+        'library/LastImportPage.vala',
+        'library/OfflinePage.vala',
+        'library/TrashPage.vala',
+        'direct/Direct.vala',
+        'direct/DirectWindow.vala',
+        'direct/DirectPhoto.vala',
+        'direct/DirectPhotoPage.vala',
+        'direct/DirectView.vala',
+        'core/Core.vala',
+        'core/DataCollection.vala',
+        'core/DataSet.vala',
+        'core/util.vala',
+        'core/SourceCollection.vala',
+        'core/SourceHoldingTank.vala',
+        'core/DatabaseSourceCollection.vala',
+        'core/ContainerSourceCollection.vala',
+        'core/ViewCollection.vala',
+        'core/DataObject.vala',
+        'core/Alteration.vala',
+        'core/DataSource.vala',
+        'core/DataSourceTypes.vala',
+        'core/DataView.vala',
+        'core/DataViewTypes.vala',
+        'core/Tracker.vala',
+        'core/SourceInterfaces.vala',
+        'sidebar/Sidebar.vala',
+        'sidebar/Branch.vala',
+        'sidebar/Entry.vala',
+        'sidebar/Tree.vala',
+        'sidebar/common.vala',
+        'events/Events.vala',
+        'events/EventsBranch.vala',
+        'events/EventsDirectoryPage.vala',
+        'events/EventPage.vala',
+        'events/EventDirectoryItem.vala',
+        'tags/Tags.vala',
+        'tags/TagsBranch.vala',
+        'tags/TagPage.vala',
+        'tags/HierarchicalTagIndex.vala',
+        'tags/HierarchicalTagUtilities.vala',
+        'camera/Camera.vala',
+        'camera/CameraBranch.vala',
+        'camera/CameraTable.vala',
             'camera/DiscoveredCamera.vala',
-            'camera/GPhoto.vala',
-            'camera/ImportPage.vala',
-            'searches/Searches.vala',
-            'searches/SearchesBranch.vala',
-            'searches/SearchBoolean.vala',
-            'searches/SavedSearchPage.vala',
-            'searches/SavedSearchDialog.vala',
-            'config/Config.vala',
-            'config/ConfigurationInterfaces.vala',
-            'config/GSettingsEngine.vala',
-            'data_imports/DataImports.vala',
-            'data_imports/DataImportsPluginHost.vala',
-            'data_imports/DataImportsUI.vala',
-            'data_imports/DataImportJob.vala',
-            'data_imports/DataImportSource.vala',
-            'folders/Folders.vala',
-            'folders/FoldersBranch.vala',
-            'folders/FoldersPage.vala',
-            'import-roll/ImportRollBranch.vala',
-            'import-roll/ImportRollEntry.vala',
-            'main.vala',
-            'AppWindow.vala',
-            'CollectionPage.vala',
-            'NaturalCollate.vala',
-            'Thumbnail.vala',
-            'ThumbnailCache.vala',
-            'CheckerboardItem.vala',
-            'CheckerboardItemText.vala',
-            'CheckerboardLayout.vala',
-            'PhotoPage.vala',
-            'Page.vala',
-            'SinglePhotoPage.vala',
-            'CheckerboardPage.vala',
-            'DragAndDropHandler.vala',
-            'PageMessagePane.vala',
-            'SortedList.vala',
-            'Dimensions.vala',
-            'Box.vala',
-            'Photo.vala',
-            'Orientation.vala',
-            'MapWidget.vala',
-            'BatchImport.vala',
-            'Dialogs.vala',
-            'Resources.vala',
-            'Debug.vala',
-            'Properties.vala',
-            'Event.vala',
-            'International.vala',
-            'AppDirs.vala',
-            'PixbufCache.vala',
-            'CommandManager.vala',
-            'Commands.vala',
-            'SlideshowPage.vala',
-            'LibraryFiles.vala',
-            'Printing.vala',
-            'Tag.vala',
-            'Screensaver.vala',
-            'Exporter.vala',
-            'DirectoryMonitor.vala',
-            'LibraryMonitor.vala',
-            'VideoSupport.vala',
-            'Tombstone.vala',
-            'MetadataWriter.vala',
-            'Application.vala',
-            'TimedQueue.vala',
-            'MediaPage.vala',
-            'MediaDataRepresentation.vala',
-            'DesktopIntegration.vala',
-            'MediaInterfaces.vala',
-            'MediaMetadata.vala',
-            'VideoMetadata.vala',
-            'MediaMonitor.vala',
-            'PhotoMonitor.vala',
-            'VideoMonitor.vala',
-            'SearchFilter.vala',
-            'MediaViewTracker.vala',
-            'UnityProgressBar.vala',
-            'Upgrades.vala',
-            'dialogs/AdjustDateTimeDialog.vala',
-            'dialogs/EntryMultiCompletion.vala',
-            'dialogs/ExportDialog.vala',
-            'dialogs/MultiTextEntryDialog.vala',
-            'dialogs/Preferences.vala',
-            'dialogs/ProgressDialog.vala',
-            'dialogs/SetBackgroundSlideshow.vala',
-            'dialogs/SetBackground.vala',
-            'dialogs/TextEntry.vala',
-            'dialogs/WelcomeDialog.vala',
-            'Profiles.vala',
-            '.unitize/_UnitInternals.vala',
-            '.unitize/_UtilInternals.vala',
-            '.unitize/_ThreadsInternals.vala',
-            '.unitize/_DbInternals.vala',
-            '.unitize/_EditingToolsInternals.vala',
-            '.unitize/_PluginsInternals.vala',
-            '.unitize/_SlideshowInternals.vala',
-            '.unitize/_PhotosInternals.vala',
-            '.unitize/_PublishingInternals.vala',
-            '.unitize/_LibraryInternals.vala',
-            '.unitize/_DirectInternals.vala',
-            '.unitize/_CoreInternals.vala',
-            '.unitize/_SidebarInternals.vala',
-            '.unitize/_EventsInternals.vala',
-            '.unitize/_TagsInternals.vala',
-            '.unitize/_CameraInternals.vala',
-            '.unitize/_SearchesInternals.vala',
-            '.unitize/_ConfigInternals.vala',
-            '.unitize/_DataImportsInternals.vala',
-            '.unitize/_FoldersInternals.vala',
-            '.unitize/_Library_unitize_entry.vala',
-            '.unitize/_Direct_unitize_entry.vala'] + shotwell_resources + face_sources,
-           include_directories : vapi_incdir,
-           dependencies : [
-               shotwell_deps,
-               sw_publishing_gui
-           ],
-           vala_args : ['--pkg', 'libgphoto2',
-                        '--pkg', 'libraw',
-                        '--pkg', 'libexif',
-                        '--pkg', 'version',
-                        '--gresources',
-                        join_paths(meson.source_root(), 'data',
-                          'org.gnome.Shotwell.gresource.xml')
-                       ],
-           link_with: [
-               sw_graphics_processor
-           ],
-           install : true)
+        'camera/GPhoto.vala',
+        'camera/ImportPage.vala',
+        'searches/Searches.vala',
+        'searches/SearchesBranch.vala',
+        'searches/SearchBoolean.vala',
+        'searches/SavedSearchPage.vala',
+        'searches/SavedSearchDialog.vala',
+        'config/Config.vala',
+        'config/ConfigurationInterfaces.vala',
+        'config/GSettingsEngine.vala',
+        'data_imports/DataImports.vala',
+        'data_imports/DataImportsPluginHost.vala',
+        'data_imports/DataImportsUI.vala',
+        'data_imports/DataImportJob.vala',
+        'data_imports/DataImportSource.vala',
+        'folders/Folders.vala',
+        'folders/FoldersBranch.vala',
+        'folders/FoldersPage.vala',
+        'import-roll/ImportRollBranch.vala',
+        'import-roll/ImportRollEntry.vala',
+        'main.vala',
+        'AppWindow.vala',
+        'CollectionPage.vala',
+        'NaturalCollate.vala',
+        'Thumbnail.vala',
+        'ThumbnailCache.vala',
+        'CheckerboardItem.vala',
+        'CheckerboardItemText.vala',
+        'CheckerboardLayout.vala',
+        'PhotoPage.vala',
+        'Page.vala',
+        'SinglePhotoPage.vala',
+        'CheckerboardPage.vala',
+        'DragAndDropHandler.vala',
+        'PageMessagePane.vala',
+        'SortedList.vala',
+        'Dimensions.vala',
+        'Box.vala',
+        'Photo.vala',
+        'Orientation.vala',
+        'MapWidget.vala',
+        'BatchImport.vala',
+        'Dialogs.vala',
+        'Resources.vala',
+        'Debug.vala',
+        'Properties.vala',
+        'Event.vala',
+        'International.vala',
+        'AppDirs.vala',
+        'PixbufCache.vala',
+        'CommandManager.vala',
+        'Commands.vala',
+        'SlideshowPage.vala',
+        'LibraryFiles.vala',
+        'Printing.vala',
+        'Tag.vala',
+        'Screensaver.vala',
+        'Exporter.vala',
+        'DirectoryMonitor.vala',
+        'LibraryMonitor.vala',
+        'VideoSupport.vala',
+        'Tombstone.vala',
+        'MetadataWriter.vala',
+        'Application.vala',
+        'TimedQueue.vala',
+        'MediaPage.vala',
+        'MediaDataRepresentation.vala',
+        'DesktopIntegration.vala',
+        'MediaInterfaces.vala',
+        'MediaMetadata.vala',
+        'VideoMetadata.vala',
+        'MediaMonitor.vala',
+        'PhotoMonitor.vala',
+        'VideoMonitor.vala',
+        'SearchFilter.vala',
+        'MediaViewTracker.vala',
+        'UnityProgressBar.vala',
+        'Upgrades.vala',
+        'dialogs/AdjustDateTimeDialog.vala',
+        'dialogs/EntryMultiCompletion.vala',
+        'dialogs/ExportDialog.vala',
+        'dialogs/MultiTextEntryDialog.vala',
+        'dialogs/Preferences.vala',
+        'dialogs/ProgressDialog.vala',
+        'dialogs/SetBackgroundSlideshow.vala',
+        'dialogs/SetBackground.vala',
+        'dialogs/TextEntry.vala',
+        'dialogs/WelcomeDialog.vala',
+        'Profiles.vala',
+        '.unitize/_UnitInternals.vala',
+        '.unitize/_UtilInternals.vala',
+        '.unitize/_ThreadsInternals.vala',
+        '.unitize/_DbInternals.vala',
+        '.unitize/_EditingToolsInternals.vala',
+        '.unitize/_PluginsInternals.vala',
+        '.unitize/_SlideshowInternals.vala',
+        '.unitize/_PhotosInternals.vala',
+        '.unitize/_PublishingInternals.vala',
+        '.unitize/_LibraryInternals.vala',
+        '.unitize/_DirectInternals.vala',
+        '.unitize/_CoreInternals.vala',
+        '.unitize/_SidebarInternals.vala',
+        '.unitize/_EventsInternals.vala',
+        '.unitize/_TagsInternals.vala',
+        '.unitize/_CameraInternals.vala',
+        '.unitize/_SearchesInternals.vala',
+        '.unitize/_ConfigInternals.vala',
+        '.unitize/_DataImportsInternals.vala',
+        '.unitize/_FoldersInternals.vala',
+        '.unitize/_Library_unitize_entry.vala',
+        '.unitize/_Direct_unitize_entry.vala',
+        'video-support/VideoReader.vala',
+        'video-support/VideoImportParams.vala',
+        'video-support/Video.vala',
+        'video-support/VideoSourceCollection.vala'
+    ] + shotwell_resources + face_sources,
+    include_directories : vapi_incdir,
+    dependencies : [
+        shotwell_deps,
+        sw_publishing_gui
+    ],
+    vala_args : [
+        '--pkg', 'libgphoto2',
+        '--pkg', 'libraw',
+        '--pkg', 'libexif',
+        '--pkg', 'version',
+        '--gresources',
+        join_paths(meson.source_root(), 'data',
+        'org.gnome.Shotwell.gresource.xml')
+    ],
+    link_with: [
+        sw_graphics_processor
+    ],
+    install : true
+)
diff --git a/src/VideoSupport.vala b/src/video-support/Video.vala
similarity index 56%
rename from src/VideoSupport.vala
rename to src/video-support/Video.vala
index 91f06330..b4c798a0 100644
--- a/src/VideoSupport.vala
+++ b/src/video-support/Video.vala
@@ -4,337 +4,32 @@
  * 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 Subprocess? thumbnailer_process = null;
-    public DateTime? timestamp { get; private set; default = null; }
-
-    public VideoReader(File file) {
-        this.file = file;
-     }
-    
-    public static bool is_supported_video_file(File file) {
-        var mime_type = ContentType.guess(file.get_basename(), new uchar[0], null);
-        // special case: deep-check content-type of files ending with .ogg
-        if (mime_type == "audio/ogg" && file.has_uri_scheme("file")) {
-            try {
-                var info = file.query_info(FileAttribute.STANDARD_CONTENT_TYPE,
-                                           FileQueryInfoFlags.NONE);
-                var content_type = info.get_content_type();
-                if (content_type != null && content_type.has_prefix ("video/")) {
-                    return true;
-                }
-            } catch (Error error) {
-                debug("Failed to query content type: %s", error.message);
-            }
-        }
-
-        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);
-        // Guessed mp4 from filename has application/ as prefix, so check for mp4 in the end
-        if (mime_type.has_prefix ("video/") || mime_type.has_suffix("mp4")) {
-            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 {
-            debug("Skipping %s, unsupported mime type %s", filename, mime_type);
-            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)) {
-                // possible for get_date() to return true and a null Date
-                if (video_date != null) {
-                    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_process != null) {
-            thumbnailer_process.force_exit();
-        }
-
-        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());
-        try {
-            thumbnailer_process = new GLib.Subprocess(SubprocessFlags.STDOUT_PIPE,
-                                          AppDirs.get_thumbnailer_bin().get_path(),
-                                          video_file);
-            debug("Spawned thumbnailer, child id: %s", thumbnailer_process.get_identifier());
-        } catch (Error e) {
-            debug("Error spawning process: %s", e.message);
-            return null;
-        }
-        
-        // Start timer.
-        var timeout = Timeout.add(THUMBNAILER_TIMEOUT, on_thumbnailer_timer);
-        
-        // Read pixbuf from stream.
-        Gdk.Pixbuf? buf = null;
-        try {
-            Bytes pixbuf_bytes;
-            thumbnailer_process.communicate(null, null, out pixbuf_bytes, null);
-            var loader = new Gdk.PixbufLoader.with_type("png");
-            loader.write_bytes(pixbuf_bytes);
-            loader.close();
-            buf = loader.get_pixbuf();
-        } catch (Error e) {
-            debug("Error creating pixbuf: %s", e.message);
-            buf = null;
-        }
-        
-        thumbnailer_process = null;
-        Source.remove (timeout);
-
-        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(),
@@ -343,7 +38,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
             }
         }
     }
-    
+
     private static bool interpreter_state_changed;
     private static int current_state;
     private static bool normal_regen_complete;
@@ -351,13 +46,13 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
     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);
     }
@@ -370,7 +65,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
         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
         unowned string[] args = null;
@@ -403,7 +98,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
         }
 
         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>();
@@ -411,17 +106,17 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
         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);
         }
@@ -430,11 +125,11 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
         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;
@@ -468,12 +163,12 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
 
     public static void terminate() {
     }
-    
+
     public static ExporterUI? export_many(Gee.Collection<Video> videos, Exporter.CompletionCallback done,
-        bool export_in_place = false) {       
+        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,
@@ -489,11 +184,11 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
                 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);
@@ -502,7 +197,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
                 AppWindow.get_instance().set_normal_cursor();
                 export_error_dialog(save_as, false);
             }
-            
+
             return null;
         }
 
@@ -510,7 +205,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
         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);
@@ -518,7 +213,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
         return exporter;
     }
 
-    protected override void commit_backlinks(SourceCollection? sources, string? backlinks) {        
+    protected override void commit_backlinks(SourceCollection? sources, string? backlinks) {
         try {
             VideoTable.get_instance().update_backlinks(get_video_id(), backlinks);
             lock (backing_row) {
@@ -548,10 +243,10 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
         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())
@@ -559,13 +254,13 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
         } 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());
@@ -573,17 +268,17 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
             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[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);
     }
@@ -596,21 +291,21 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
 
     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;
     }
@@ -624,7 +319,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
     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;
@@ -633,7 +328,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
 
     public override void set_title(string? title) {
         string? new_title = prep_title(title);
-        
+
         lock (backing_row) {
             if (backing_row.title == new_title)
                 return;
@@ -660,7 +355,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
 
     public override bool set_comment(string? comment) {
         string? new_comment = prep_title(comment);
-        
+
         lock (backing_row) {
             if (backing_row.comment == new_comment)
                 return true;
@@ -675,7 +370,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
             // 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;
@@ -730,10 +425,10 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
     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();
     }
@@ -741,47 +436,47 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
     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 {
@@ -791,10 +486,10 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
             }
             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);
@@ -804,99 +499,99 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
     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();
@@ -906,111 +601,111 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
             // 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;
@@ -1018,195 +713,25 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
             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();
-    }
-}
diff --git a/src/video-support/VideoImportParams.vala b/src/video-support/VideoImportParams.vala
new file mode 100644
index 00000000..67e5f0fa
--- /dev/null
+++ b/src/video-support/VideoImportParams.vala
@@ -0,0 +1,28 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+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;
+    }
+}
diff --git a/src/video-support/VideoReader.vala b/src/video-support/VideoReader.vala
new file mode 100644
index 00000000..8343facd
--- /dev/null
+++ b/src/video-support/VideoReader.vala
@@ -0,0 +1,302 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * 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 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) {
+        var mime_type = ContentType.guess(file.get_basename(), new uchar[0], null);
+        // special case: deep-check content-type of files ending with .ogg
+        if (mime_type == "audio/ogg" && file.has_uri_scheme("file")) {
+            try {
+                var info = file.query_info(FileAttribute.STANDARD_CONTENT_TYPE,
+                                           FileQueryInfoFlags.NONE);
+                var content_type = info.get_content_type();
+                if (content_type != null && content_type.has_prefix ("video/")) {
+                    return true;
+                }
+            } catch (Error error) {
+                debug("Failed to query content type: %s", error.message);
+            }
+        }
+
+        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);
+        // Guessed mp4 from filename has application/ as prefix, so check for mp4 in the end
+        if (mime_type.has_prefix ("video/") || mime_type.has_suffix("mp4")) {
+            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 {
+            debug("Skipping %s, unsupported mime type %s", filename, mime_type);
+            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)) {
+                // possible for get_date() to return true and a null Date
+                if (video_date != null) {
+                    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);
+#if VALA_0_40
+            Posix.kill(thumbnailer_pid, Posix.Signal.KILL);
+#else
+            Posix.kill(thumbnailer_pid, Posix.SIGKILL);
+#endif
+        }
+        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 != Process.exit_status(child_status)) {
+            debug("Thumbnailer exited with error code: %d",
+                    Process.exit_status(child_status));
+            buf = null;
+        }
+
+        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;
+    }
+}
diff --git a/src/video-support/VideoSourceCollection.vala b/src/video-support/VideoSourceCollection.vala
new file mode 100644
index 00000000..d6eddca6
--- /dev/null
+++ b/src/video-support/VideoSourceCollection.vala
@@ -0,0 +1,175 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+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();
+    }
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]