summaryrefslogtreecommitdiff
path: root/raphodo/rescan.py
diff options
context:
space:
mode:
Diffstat (limited to 'raphodo/rescan.py')
-rw-r--r--raphodo/rescan.py167
1 files changed, 167 insertions, 0 deletions
diff --git a/raphodo/rescan.py b/raphodo/rescan.py
new file mode 100644
index 0000000..99ce43c
--- /dev/null
+++ b/raphodo/rescan.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2011-2017 Damon Lynch <damonlynch@gmail.com>
+
+# This file is part of Rapid Photo Downloader.
+#
+# Rapid Photo Downloader is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Rapid Photo Downloader is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Rapid Photo Downloader. If not,
+# see <http://www.gnu.org/licenses/>.
+
+"""
+Given a collection of RPDFiles, rescans a camera to locate their 'new' location.
+
+Used in case of iOS and possibly other buggy devices that generate subfolders
+for photos / videos seemingly at random each time the device is initialized for access,
+which is what a gphoto2 process does.
+"""
+
+__author__ = 'Damon Lynch'
+__copyright__ = "Copyright 2011-2017, Damon Lynch"
+
+from typing import List, DefaultDict, Optional
+import logging
+from collections import defaultdict
+import os
+from itertools import chain
+
+import gphoto2 as gp
+
+from raphodo.rpdfile import RPDFile
+from raphodo.camera import Camera, CameraProblemEx
+from preferences import ScanPreferences, Preferences
+
+
+class RescanCamera:
+ """
+ Rescan a camera / smartphone looking for files that were already
+ previously scanned.
+
+ Newly updated files are stored in the member variable rpd_files, and
+ files that could not be relocated are found in member missing_rpd_files.
+
+ Assumes camera already initialized, with specific folders correctly set.
+ """
+
+ def __init__(self, camera: Camera, prefs: Preferences) -> None:
+ self.camera = camera
+ assert camera.camera_has_dcim_like_folder()
+ # Relocated RPD files
+ self.rpd_files = [] # type: List[RPDFile]
+ # Missing RPD files
+ self.missing_rpd_files = [] # type: List[RPDFile]
+ self.prefs = prefs
+ self.scan_preferences = None # type: Optional[ScanPreferences]
+
+ def rescan_camera(self, rpd_files: List[RPDFile]) -> None:
+ """
+ Determine if the files are found in the same folders as when the camera was
+ last initialized. Works around a crazy iOS bug.
+
+ :param rpd_files: if individual rpd_files are indeed located in new folders,
+ a side effect of calling this function is that the rpd_files will have their
+ paths updated, even though a new list is returned
+ """
+
+ if not rpd_files:
+ return
+ # attempt to read extract of file
+ rpd_file = rpd_files[0]
+ try:
+ self.camera.get_exif_extract(folder=rpd_file.path, file_name=rpd_file.name)
+ except CameraProblemEx as e:
+ logging.debug(
+ "Failed to read extract of sample file %s: rescanning %s",
+ rpd_file.name, self.camera.display_name
+ )
+ else:
+ # Apparently no problems accessing the first file, so let's assume the rest are
+ # fine. Let's hope that's a valid assumption.
+ logging.debug("%s did not need to be rescanned", self.camera.display_name)
+ self.rpd_files = rpd_files
+ return
+
+ # filename: RPDFile
+ self.prev_scanned_files = defaultdict(list) # type: DefaultDict[str, List[RPDFile]]
+ self.scan_preferences = ScanPreferences(self.prefs.ignored_paths)
+
+ for rpd_file in rpd_files:
+ self.prev_scanned_files[rpd_file.name].append(rpd_file)
+
+ for folders in self.camera.specific_folders:
+ for folder in folders:
+ logging.info("Rescanning %s on %s", folder, self.camera.display_name)
+ self.relocate_files_on_camera(folder)
+
+ self.missing_rpd_files = list(chain(*self.prev_scanned_files.values()))
+
+ def relocate_files_on_camera(self, path: str) -> None:
+ """
+ Recursively scan path looking for the folders in which previously located files are
+ now stored.
+
+ :param path: path to check in
+ """
+
+ files_in_folder = []
+
+ try:
+ files_in_folder = self.camera.camera.folder_list_files(path, self.camera.context)
+ except gp.GPhoto2Error as e:
+ logging.error("Unable to scan files on camera: error %s", e.code)
+
+ for name, value in files_in_folder:
+ if name in self.prev_scanned_files:
+ prev_rpd_files = self.prev_scanned_files[name]
+ if len(prev_rpd_files) > 1:
+ rpd_file = None # type: RPDFile
+ # more than one file with the same filename is found on the camera
+ # compare match by modification time and size check
+ for prev_rpd_file in prev_rpd_files:
+ modification_time, size = 0, 0
+ if prev_rpd_file.modification_time:
+ try:
+ modification_time, size = self.camera.get_file_info(path, name)
+ except gp.GPhoto2Error as e:
+ logging.error(
+ "Unable to access modification_time or size from %s on %s. "
+ "Error code: %s",
+ os.path.join(path, name), self.camera.display_name, e.code
+ )
+ if modification_time == prev_rpd_file.modification_time and size == \
+ prev_rpd_file.size:
+ rpd_file = prev_rpd_file
+ prev_rpd_files.remove(prev_rpd_file)
+ break
+ else:
+ rpd_file = prev_rpd_files[0]
+ del self.prev_scanned_files[name]
+
+ if rpd_file:
+ rpd_file.path = path
+ self.rpd_files.append(rpd_file)
+
+ # Recurse over subfolders in which we should
+ folders = []
+ try:
+ for name, value in self.camera.camera.folder_list_folders(path, self.camera.context):
+ if self.scan_preferences.scan_this_path(os.path.join(path, name)):
+ folders.append(name)
+ except gp.GPhoto2Error as e:
+ logging.error(
+ "Unable to scan files on %s. Error code: %s",
+ self.camera.display_name, e.code
+ )
+
+ for name in folders:
+ self.relocate_files_on_camera(os.path.join(path, name)) \ No newline at end of file