diff options
Diffstat (limited to 'rapid')
-rw-r--r-- | rapid/ChangeLog | 29 | ||||
-rw-r--r-- | rapid/backupfile.py | 29 | ||||
-rw-r--r-- | rapid/config.py | 2 | ||||
-rw-r--r-- | rapid/copyfiles.py | 107 | ||||
-rw-r--r-- | rapid/generatename.py | 33 | ||||
-rw-r--r-- | rapid/glade3/about.ui | 3 | ||||
-rw-r--r-- | rapid/misc.py | 14 | ||||
-rw-r--r-- | rapid/preferencesdialog.py | 28 | ||||
-rwxr-xr-x | rapid/rapid.py | 54 | ||||
-rw-r--r-- | rapid/rpdfile.py | 153 | ||||
-rwxr-xr-x | rapid/scan.py | 128 | ||||
-rw-r--r-- | rapid/subfolderfile.py | 91 |
12 files changed, 439 insertions, 232 deletions
diff --git a/rapid/ChangeLog b/rapid/ChangeLog index cc27252..8269afb 100644 --- a/rapid/ChangeLog +++ b/rapid/ChangeLog @@ -1,3 +1,30 @@ +Version 0.4.7 +------------- + +2013-10-19 + +Added feature to download audio files that are associated with photos such as +those created by the Canon 1D series of cameras. + +Fixed bug #1242119: Choosing a new folder does not work in Ubuntu 13.10. In +Ubuntu 13.10, choosing a destination or source folder from its bookmark does not +work. The correct value is displayed in the file chooser button, but this value +is not used by Rapid Photo Downloader. + +Fixed bug #1206853: Crashes when system message notifications not functioning +properly. + +Fixed bug #909405: Allow selections by row (and not GTK default by square) when +user is dragging the mouse or using the keyboard to select. Thank you to +user 'Salukibob' for the patch. + +Added a KDE Solid action. Solid is KDE4's hardware-related framework. It detects +when the user connects a new device and display a list of related actions. +Thanks to dju` for the patch. + +Added Belarusian translation -- thanks go to Ilya Tsimokhin. Updated Swedish and +Ukrainian translations. + Version 0.4.6 ------------- @@ -11,7 +38,7 @@ Added extra debugging output to help trace program execution progress. Updated German and Spanish translations. Version 0.4.6 Beta 1 -------------- +-------------------- 2012-11-26 diff --git a/rapid/backupfile.py b/rapid/backupfile.py index 310e665..7c91a19 100644 --- a/rapid/backupfile.py +++ b/rapid/backupfile.py @@ -54,7 +54,7 @@ class BackupFiles(multiprocessing.Process): # As of Ubuntu 12.10 / Fedora 18, the file move/rename command is running agonisingly slowly # A hackish workaround is to replace it with the standard python function - self.use_gnome_file_operations = True + self.use_gnome_file_operations = False def check_termination_request(self): """ @@ -99,12 +99,22 @@ class BackupFiles(multiprocessing.Process): """Backs up small files like XMP or THM files""" source = gio.File(full_file_name) dest_name = os.path.join(dest_dir, os.path.split(full_file_name)[1]) - logger.debug("Backing up %s", dest_name) - dest=gio.File(dest_name) - try: - source.copy(dest, self.progress_callback_no_update, cancellable=None) - except gio.Error, inst: - logger.error("Failed to backup file %s", full_file_name) + + if self.use_gnome_file_operations: + logger.debug("Backing up additional file %s...", dest_name) + dest=gio.File(dest_name) + try: + source.copy(dest, self.progress_callback_no_update, cancellable=None) + logger.debug("...backing up additional file %s succeeded", dest_name) + except gio.Error, inst: + logger.error("Failed to backup file %s: %s", full_file_name, inst) + else: + try: + logger.debug("Using python to back up additional file %s...", dest_name) + shutil.copy(full_file_name, dest_name) + logger.debug("...backing up additional file %s succeeded", dest_name) + except: + logger.error("Backup of %s failed", full_file_name) def run(self): @@ -209,10 +219,13 @@ class BackupFiles(multiprocessing.Process): else: rpd_file.status = config.STATUS_BACKUP_PROBLEM else: - # backup any THM or XMP files + # backup any THM, audio or XMP files if rpd_file.download_thm_full_name: self.backup_additional_file(dest_dir, rpd_file.download_thm_full_name) + if rpd_file.download_audio_full_name: + self.backup_additional_file(dest_dir, + rpd_file.download_audio_full_name) if rpd_file.download_xmp_full_name: self.backup_additional_file(dest_dir, rpd_file.download_xmp_full_name) diff --git a/rapid/config.py b/rapid/config.py index 977c342..1a12ea7 100644 --- a/rapid/config.py +++ b/rapid/config.py @@ -16,7 +16,7 @@ ### Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 ### USA -version = '0.4.6' +version = '0.4.7' GCONF_KEY="/apps/rapid-photo-downloader" diff --git a/rapid/copyfiles.py b/rapid/copyfiles.py index 4a67ece..5bc243c 100644 --- a/rapid/copyfiles.py +++ b/rapid/copyfiles.py @@ -45,8 +45,8 @@ class CopyFiles(multiprocessing.Process): """ def __init__(self, photo_download_folder, video_download_folder, files, modify_files_during_download, modify_pipe, - scan_pid, - batch_size_MB, results_pipe, terminate_queue, + scan_pid, + batch_size_MB, results_pipe, terminate_queue, run_event): multiprocessing.Process.__init__(self) self.results_pipe = results_pipe @@ -60,7 +60,7 @@ class CopyFiles(multiprocessing.Process): self.scan_pid = scan_pid self.no_files= len(self.files) self.run_event = run_event - + def check_termination_request(self): """ Check to see this process has not been requested to immediately terminate @@ -71,8 +71,8 @@ class CopyFiles(multiprocessing.Process): logger.info("Terminating file copying") return True return False - - + + def update_progress(self, amount_downloaded, total): # first check if process is being terminated if not self.terminate_queue.empty(): @@ -86,65 +86,65 @@ class CopyFiles(multiprocessing.Process): if amount_downloaded == total: # this function is called a couple of times when total is reached self.total_reached = True - + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_BYTES, (self.scan_pid, self.total_downloaded + amount_downloaded, chunk_downloaded)))) if amount_downloaded == total: self.bytes_downloaded = 0 - + def progress_callback(self, amount_downloaded, total): self.update_progress(amount_downloaded, total) - + def thm_progress_callback(self, amount_downloaded, total): # we don't care about tracking download progress for tiny THM files! pass - + def run(self): """start the actual copying of files""" - + #characters used to generate temporary filenames filename_characters = string.letters + string.digits - + self.bytes_downloaded = 0 self.total_downloaded = 0 - + self.cancel_copy = gio.Cancellable() - + self.create_temp_dirs() - + # Send the location of both temporary directories, so they can be # removed once another process attempts to rename all the files in them # and move them to generated subfolders - self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_TEMP_DIRS, + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_TEMP_DIRS, (self.scan_pid, self.photo_temp_dir, self.video_temp_dir)))) - + if self.photo_temp_dir or self.video_temp_dir: - + self.thumbnail_maker = tn.Thumbnail() - + for i in range(self.no_files): rpd_file = self.files[i] self.total_reached = False - + # pause if instructed by the caller self.run_event.wait() - + if self.check_termination_request(): return None - + source = gio.File(path=rpd_file.full_file_name) - + #generate temporary name 5 digits long, no extension temp_name = ''.join(random.choice(filename_characters) for i in xrange(5)) - + temp_full_file_name = os.path.join( - self._get_dest_dir(rpd_file.file_type), + self._get_dest_dir(rpd_file.file_type), temp_name) rpd_file.temp_full_file_name = temp_full_file_name dest = gio.File(path=temp_full_file_name) - + copy_succeeded = False try: source.copy(dest, self.progress_callback, cancellable=self.cancel_copy) @@ -154,24 +154,24 @@ class CopyFiles(multiprocessing.Process): pn.DOWNLOAD_COPYING_ERROR_W_NO, {'filetype': rpd_file.title}) rpd_file.add_extra_detail( - pn.DOWNLOAD_COPYING_ERROR_W_NO_DETAIL, + pn.DOWNLOAD_COPYING_ERROR_W_NO_DETAIL, {'errorno': inst.code, 'strerror': inst.message}) - + rpd_file.status = config.STATUS_DOWNLOAD_FAILED - + rpd_file.error_title = rpd_file.problem.get_title() rpd_file.error_msg = _("%(problem)s\nFile: %(file)s") % \ {'problem': rpd_file.problem.get_problems(), 'file': rpd_file.full_file_name} - + logger.error("Failed to download file: %s", rpd_file.full_file_name) logger.error(inst) self.update_progress(rpd_file.size, rpd_file.size) - + # increment this amount regardless of whether the copy actually # succeeded or not. It's neccessary to keep the user informed. self.total_downloaded += rpd_file.size - + # copy THM (video thumbnail file) if there is one if copy_succeeded and rpd_file.thm_full_name: source = gio.File(path=rpd_file.thm_full_name) @@ -186,8 +186,21 @@ class CopyFiles(multiprocessing.Process): logger.error("Failed to download video THM file: %s", rpd_file.thm_full_name) else: temp_thm_full_name = None - - + + #copy audio file if there is one + if copy_succeeded and rpd_file.audio_file_full_name: + source = gio.File(path=rpd_file.audio_file_full_name) + # reuse photo's file name + temp_audio_full_name = temp_full_file_name + '__rpd__audio' + dest = gio.File(path=temp_audio_full_name) + try: + source.copy(dest, self.thm_progress_callback, cancellable=self.cancel_copy) + rpd_file.temp_audio_full_name = temp_audio_full_name + logger.debug("Copied audio file %s", rpd_file.temp_audio_full_name) + except gio.Error, inst: + logger.error("Failed to download audio file: %s", rpd_file.audio_file_full_name) + + if copy_succeeded and rpd_file.generate_thumbnail: thumbnail, thumbnail_icon = self.thumbnail_maker.get_thumbnail( temp_full_file_name, @@ -197,33 +210,33 @@ class CopyFiles(multiprocessing.Process): else: thumbnail = None thumbnail_icon = None - + if rpd_file.metadata is not None: rpd_file.metadata = None - - + + download_count = i + 1 if self.modify_files_during_download and copy_succeeded: copy_finished = download_count == self.no_files - - self.modify_pipe.send((rpd_file, download_count, temp_full_file_name, + + self.modify_pipe.send((rpd_file, download_count, temp_full_file_name, thumbnail_icon, thumbnail, copy_finished)) else: - self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_FILE, + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_FILE, (copy_succeeded, rpd_file, download_count, - temp_full_file_name, + temp_full_file_name, thumbnail_icon, thumbnail)))) - - + + self.results_pipe.send((rpdmp.CONN_COMPLETE, None)) - + def _get_dest_dir(self, file_type): if file_type == rpdfile.FILE_TYPE_PHOTO: return self.photo_temp_dir else: return self.video_temp_dir - + def _create_temp_dir(self, folder): try: temp_dir = tempfile.mkdtemp(prefix="rpd-tmp-", dir=folder) @@ -234,15 +247,15 @@ class CopyFiles(multiprocessing.Process): strerror, folder) temp_dir = None - + return temp_dir - + def create_temp_dirs(self): self.photo_temp_dir = self.video_temp_dir = None if self.photo_download_folder is not None: self.photo_temp_dir = self._create_temp_dir(self.photo_download_folder) if self.video_download_folder is not None: self.video_temp_dir = self._create_temp_dir(self.photo_download_folder) - + diff --git a/rapid/generatename.py b/rapid/generatename.py index 6d138e8..446aaf8 100644 --- a/rapid/generatename.py +++ b/rapid/generatename.py @@ -113,19 +113,34 @@ class PhotoName: logger.error("Both file modification time and metadata date & time are invalid for file %s", self.rpd_file.full_file_name) return '' - def _get_thm_extension(self): + def _get_associated_file_extension(self, associate_file): """ - Generates THM extension with correct capitalization, if needed + Generates extensions with correct capitalization for files like + thumbnail or audio files. """ - if self.rpd_file.thm_full_name: - thm_extension = os.path.splitext(self.rpd_file.thm_full_name)[1] + if associate_file: + extension = os.path.splitext(associate_file)[1] if self.L2 == UPPERCASE: - thm_extension = thm_extension.upper() + extension = extension.upper() elif self.L2 == LOWERCASE: - thm_extension = thm_extension.lower() - self.rpd_file.thm_extension = thm_extension + extension = extension.lower() else: - self.rpd_file.thm_extension = None + extension = None + return extension + + + def _get_thm_extension(self): + """ + Generates THM extension with correct capitalization, if needed + """ + self.rpd_file.thm_extension = self._get_associated_file_extension(self.rpd_file.thm_full_name) + + def _get_audio_extension(self): + """ + Generates audio extension with correct capitalization, if needed + e.g. WAV or wav + """ + self.rpd_file.audio_extension = self._get_associated_file_extension(self.rpd_file.audio_file_full_name) def _get_xmp_extension(self, extension): """ @@ -156,11 +171,13 @@ class PhotoName: if self.L1 == NAME_EXTENSION: filename = self.rpd_file.name self._get_thm_extension() + self._get_audio_extension() self._get_xmp_extension(extension) elif self.L1 == NAME: filename = name elif self.L1 == EXTENSION: self._get_thm_extension() + self._get_audio_extension() self._get_xmp_extension(extension) if extension: if not self.strip_initial_period_from_extension: diff --git a/rapid/glade3/about.ui b/rapid/glade3/about.ui index 221c62e..0a82e74 100644 --- a/rapid/glade3/about.ui +++ b/rapid/glade3/about.ui @@ -34,6 +34,7 @@ Torben Gundtofte-Bruun <torben@g-b.dk> Miroslav Matejaš <silverspace@ubuntu-hr.org> Nicolás M. Zahlut <nzahlut@live.com> Erik M +Toni Lähdekorpi <toni@lygon.net> Jose Luis Navarro <jlnavarro111@gmail.com> Tomas Novak <kuvaly@seznam.cz> Abel O'Rian <abel.orian@gmail.com> @@ -47,7 +48,7 @@ Mikko Ruohola <polarfox@polarfox.net> Ahmed Shubbar <ahmed.shubbar@gmail.com> Sergei Sedov <sedov@webmail.perm.ru> Marco Solari <marcosolari@gmail.com> -Toni Lähdekorpi <toni@lygon.net> +Ilya Tsimokhin <ilya@tsimokhin.com> Ulf Urdén <ulf.urden@purplescout.com> Julien Valroff <julien@kirya.net> Aron Xu <happyaron.xu@gmail.com> diff --git a/rapid/misc.py b/rapid/misc.py index 69b4a93..a2c6d08 100644 --- a/rapid/misc.py +++ b/rapid/misc.py @@ -19,6 +19,8 @@ ### USA # modified by Damon Lynch 2009 to remove default bold formatting and alignment +# modified by Damon Lynch 2103 to add function to get folder chosen by user in file chooser button + """Module of commonly used helper classes and functions """ @@ -44,3 +46,15 @@ def run_dialog( text, secondarytext=None, parent=None, messagetype=gtk.MESSAGE_ ret = d.run() d.destroy() return ret + +def get_folder_selection(filechooserbutton): + """ + Returns the path (folder) the user has chosen in a filechooserbutton + """ + # this no longer works on Ubuntu 13.10: + # path = filechooserbutton.get_current_folder() + # but this works on Ubuntu 13.10: + path = filechooserbutton.get_filenames() #returns a list + if path: + path = path[0] + return path diff --git a/rapid/preferencesdialog.py b/rapid/preferencesdialog.py index 6e1f548..295b6e7 100644 --- a/rapid/preferencesdialog.py +++ b/rapid/preferencesdialog.py @@ -965,23 +965,33 @@ class PreferencesDialog(): self.treeview.set_cursor(0,column) def on_download_folder_filechooser_button_selection_changed(self, widget): - self.prefs.download_folder = widget.get_current_folder() - self.update_photo_download_folder_example() + path = misc.get_folder_selection(widget) + if path: + self.prefs.download_folder = path + self.update_photo_download_folder_example() def on_video_download_folder_filechooser_button_selection_changed(self, widget): - self.prefs.video_download_folder = widget.get_current_folder() - self.update_video_download_folder_example() + path = misc.get_folder_selection(widget) + if path: + self.prefs.video_download_folder = path + self.update_video_download_folder_example() def on_backup_folder_filechooser_button_selection_changed(self, widget): - self.prefs.backup_location = widget.get_current_folder() - self.update_backup_example() + path = misc.get_folder_selection(widget) + if path: + self.prefs.backup_location = path + self.update_backup_example() def on_backup_video_folder_filechooser_button_selection_changed(self, widget): - self.prefs.backup_video_location = widget.get_current_folder() - self.update_backup_example() + path = misc.get_folder_selection(widget) + if path: + self.prefs.backup_video_location = path + self.update_backup_example() def on_device_location_filechooser_button_selection_changed(self, widget): - self.prefs.device_location = widget.get_current_folder() + path = misc.get_folder_selection(widget) + if path: + self.prefs.device_location = path def on_add_ignored_path_button_clicked(self, widget): i = IgnorePathDialog(parent_window = self.dialog, diff --git a/rapid/rapid.py b/rapid/rapid.py index 7de18c2..4f7e6bc 100755 --- a/rapid/rapid.py +++ b/rapid/rapid.py @@ -54,6 +54,8 @@ logger = log_to_stderr() import rpdfile +from misc import get_folder_selection + import problemnotification as pn import thumbnail as tn import rpdmultiprocessing as rpdmp @@ -343,7 +345,10 @@ class DeviceCollection(gtk.TreeView): n = pynotify.Notification(title, message) n.set_icon_from_pixbuf(self.parent_app.application_icon) - n.show() + try: + n.show() + except: + logger.error("Unable to display message using notification system") def create_cairo_image_surface(pil_image, image_width, image_height): @@ -627,6 +632,20 @@ class ThumbnailDisplay(gtk.IconView): self.liststore.set_sort_column_id(self.TIMESTAMP_COL, gtk.SORT_ASCENDING) def on_selection_changed(self, iconview): + """ + Allow selections by row (and not GTK default by square) when user is + dragging the mouse or using the keyboard to select + """ + selections = self.get_selected_items() + if len(selections) > 1: + previous_sel = selections[0][0] + 1 + # seleted items list always starts with the highest selected item + for selection in selections: + current_sel = selection[0] + if current_sel <> (previous_sel-1): + for i in range(previous_sel-1, current_sel, -1): + self.select_path(i) + previous_sel = current_sel self._selected_items = self.get_selected_items() def on_checkbutton_toggled(self, cellrenderertoggle, path): @@ -1838,7 +1857,7 @@ class RapidApp(dbus.service.Object): if path: if (path in self.prefs.device_blacklist and self.search_for_PSD()): - logger.info("%s ignored", mount.get_name()) + logger.info("blacklisted device %s ignored", mount.get_name()) else: logger.info("Detected %s", mount.get_name()) is_backup_mount, backup_file_type = self.check_if_backup_mount(path) @@ -2807,7 +2826,10 @@ class RapidApp(dbus.service.Object): n = pynotify.Notification(notification_name, message) n.set_icon_from_pixbuf(icon) - n.show() + try: + n.show() + except: + logger.error("Unable to display message using notification system") def notify_download_complete(self): if self.display_summary_notification: @@ -2859,7 +2881,10 @@ class RapidApp(dbus.service.Object): if use_pynotify: n = pynotify.Notification(PROGRAM_NAME, message) n.set_icon_from_pixbuf(self.application_icon) - n.show() + try: + n.show() + except: + logger.error("Unable to display message using notification system") self.display_summary_notification = False # don't show it again unless needed @@ -3373,6 +3398,10 @@ class RapidApp(dbus.service.Object): self._set_to_toolbar_values() self.to_photo_filechooser_button.connect("selection-changed", self.on_to_photo_filechooser_button_selection_changed) + #~ self.to_photo_filechooser_button.connect("file-set", + #~ self.on_to_photo_filechooser_button_file_set) + #~ self.to_photo_filechooser_button.connect("current-folder-changed", + #~ self.on_to_photo_filechooser_button_current_folder_changed) self.to_video_filechooser_button.connect("selection-changed", self.on_to_video_filechooser_button_selection_changed) self.dest_toolbar.show_all() @@ -3426,17 +3455,28 @@ class RapidApp(dbus.service.Object): def on_from_filechooser_button_selection_changed(self, filechooserbutton): logger.debug("on_from_filechooser_button_selection_changed") - path = filechooserbutton.get_current_folder() + path = get_folder_selection(filechooserbutton) if path and not self.rerun_setup_available_image_and_video_media: self.prefs.device_location = path + def on_from_filechooser_button_file_set(self, button): + logger.debug("on_from_filechooser_button_file_set") + + def on_to_photo_filechooser_button_file_set(self, filechooserbutton): + logger.debug("on_to_filechooser_button_file_set") + def on_to_photo_filechooser_button_selection_changed(self, filechooserbutton): - path = filechooserbutton.get_current_folder() + logger.debug("on_to_filechooser_button_selection_changed") + path = get_folder_selection(filechooserbutton) + #~ logger.debug("Path: %s", path) if path: self.prefs.download_folder = path + def on_to_photo_filechooser_button_current_folder_changed(self, filechooserbutton): + logger.debug("on_to_photo_filechooser_button_current_folder_changed") + def on_to_video_filechooser_button_selection_changed(self, filechooserbutton): - path = filechooserbutton.get_current_folder() + path = get_folder_selection(filechooserbutton) if path: self.prefs.video_download_folder = path diff --git a/rapid/rpdfile.py b/rapid/rpdfile.py index a405782..3749554 100644 --- a/rapid/rpdfile.py +++ b/rapid/rpdfile.py @@ -42,20 +42,22 @@ import problemnotification as pn import thumbnail as tn -RAW_EXTENSIONS = ['arw', 'dcr', 'cr2', 'crw', 'dng', 'mos', 'mef', 'mrw', - 'nef', 'nrw', 'orf', 'pef', 'raf', 'raw', 'rw2', 'sr2', +RAW_EXTENSIONS = ['arw', 'dcr', 'cr2', 'crw', 'dng', 'mos', 'mef', 'mrw', + 'nef', 'nrw', 'orf', 'pef', 'raf', 'raw', 'rw2', 'sr2', 'srw'] - + JPEG_EXTENSIONS = ['jpg', 'jpe', 'jpeg'] NON_RAW_IMAGE_EXTENSIONS = JPEG_EXTENSIONS + ['tif', 'tiff'] PHOTO_EXTENSIONS = RAW_EXTENSIONS + NON_RAW_IMAGE_EXTENSIONS +AUDIO_EXTENSIONS = ['wav', 'mp3'] + if metadatavideo.DOWNLOAD_VIDEO: - # some distros do not include the necessary libraries that Rapid Photo Downloader + # some distros do not include the necessary libraries that Rapid Photo Downloader # needs to be able to download videos - VIDEO_EXTENSIONS = ['3gp', 'avi', 'm2t', 'mov', 'mp4', 'mpeg','mpg', 'mod', + VIDEO_EXTENSIONS = ['3gp', 'avi', 'm2t', 'mov', 'mp4', 'mpeg','mpg', 'mod', 'tod'] if metadataexiftool.EXIFTOOL_VERSION is not None: VIDEO_EXTENSIONS += ['mts'] @@ -71,7 +73,7 @@ FILE_TYPE_VIDEO = 1 def file_type(file_extension): """ Uses file extentsion to determine the type of file - photo or video. - + Returns True if yes, else False. """ if file_extension in PHOTO_EXTENSIONS: @@ -79,47 +81,50 @@ def file_type(file_extension): elif file_extension in VIDEO_EXTENSIONS: return FILE_TYPE_VIDEO return None - -def get_rpdfile(extension, name, display_name, path, size, - file_system_modification_time, thm_full_name, + +def get_rpdfile(extension, name, display_name, path, size, + file_system_modification_time, + thm_full_name, audio_file_full_name, scan_pid, file_id, file_type): - + if file_type == FILE_TYPE_VIDEO: return Video(name, display_name, path, size, file_system_modification_time, thm_full_name, + audio_file_full_name, scan_pid, file_id) else: # assume it's a photo - no check for performance reasons (this will be # called many times) return Photo(name, display_name, path, size, file_system_modification_time, thm_full_name, + audio_file_full_name, scan_pid, file_id) class FileTypeCounter: def __init__(self): self._counter = dict() - + def add(self, file_type): self._counter[file_type] = self._counter.setdefault(file_type, 0) + 1 - + def no_videos(self): """Returns the number of videos""" return self._counter.setdefault(FILE_TYPE_VIDEO, 0) - + def no_photos(self): """Returns the number of photos""" return self._counter.setdefault(FILE_TYPE_PHOTO, 0) - + def file_types_present(self): - """ + """ returns a string to be displayed to the user that can be used to show if a value refers to photos or videos or both, or just one of each """ - + no_videos = self.no_videos() no_images = self.no_photos() - + if (no_videos > 0) and (no_images > 0): v = _('photos and videos') elif (no_videos == 0) and (no_images == 0): @@ -134,14 +139,14 @@ class FileTypeCounter: v = _('photos') else: v = _('photo') - return v - + return v + def count_files(self): i = 0 for key in self._counter: i += self._counter[key] return i - + def summarize_file_count(self): """ Summarizes the total number of photos and/or videos that can be @@ -153,26 +158,27 @@ class FileTypeCounter: file_types_present = self.file_types_present() file_count_summary = _("%(number)s %(filetypes)s") % \ {'number':self.count_files(), - 'filetypes': file_types_present} + 'filetypes': file_types_present} return (file_count_summary, file_types_present) - + def running_file_count(self): """ - Displays raw numbers of photos and videos. Displayed as a scan is - occurring. + Displays raw numbers of photos and videos. Displayed as a scan is + occurring. """ return _("scanning (found %(photos)s photos and %(videos)s videos)...") % ({'photos': self.no_photos(), 'videos': self.no_videos()}) - + class RPDFile: """ Base class for photo or video file, with metadata """ - def __init__(self, name, display_name, path, size, + def __init__(self, name, display_name, path, size, file_system_modification_time, thm_full_name, + audio_file_full_name, scan_pid, file_id): - + self.path = path self.name = name @@ -180,38 +186,43 @@ class RPDFile: self.full_file_name = os.path.join(path, name) self.extension = os.path.splitext(name)[1][1:].lower() - + self.size = size # type int - + self.modification_time = file_system_modification_time - + #full path and name of thumbnail file that is associated with some videos self.thm_full_name = thm_full_name - + + #full path and name of audio file that is associated with some photos and maybe one day videos + #think Canon 1D series of cameras + self.audio_file_full_name = audio_file_full_name + self.status = config.STATUS_NOT_DOWNLOADED self.problem = None # class Problem in problemnotifcation.py - + self._assign_file_type() - + self.scan_pid = scan_pid self.file_id = file_id self.unique_id = str(scan_pid) + ":" + file_id - + self.problem = None self.job_code = None - + # indicates whether to generate a thumbnail during the copy # files process self.generate_thumbnail = False - + # generated values - + self.temp_full_file_name = '' self.temp_thm_full_name = '' + self.temp_audio_full_name = '' self.temp_xmp_full_name = '' - + self.download_start_time = None - + self.download_subfolder = '' self.download_path = '' self.download_name = '' @@ -219,63 +230,65 @@ class RPDFile: self.download_full_base_name = '' #file name with path but no extension self.download_thm_full_name = '' #name of THM (thumbnail) file with path self.download_xmp_full_name = '' #name of XMP sidecar with path - + self.download_audio_full_name = '' #name of the WAV or MP3 audio file with path + self.metadata = None - + # Values that will be inserted in download process -- # (commented out because they're not needed until then) - + #self.sequences = None #self.download_folder #self.subfolder_pref_list = [] #self.name_pref_list = [] #strip_characters = False #self.thm_extension = '' + #self.wav_extension = '' #self.xmp_extension = '' - - #these values are set only if they were written to an xmp sidecar + + #these values are set only if they were written to an xmp sidecar #in the filemodify process #self.new_aperture = '' #self.new_focal_length = '' - - + + def _assign_file_type(self): self.file_type = None - + def _load_file_for_metadata(self, temp_file): if temp_file: return self.temp_full_file_name else: - return self.full_file_name - + return self.full_file_name + def initialize_problem(self): self.problem = pn.Problem() # these next values are used to display in the error log window # the information in them can vary from other forms of display of errors self.error_title = self.error_msg = self.error_extra_detail = '' - + def has_problem(self): if self.problem is None: return False else: return self.problem.has_problem() - + def add_problem(self, component, problem_definition, *args): if self.problem is None: self.initialize_problem() self.problem.add_problem(component, problem_definition, *args) - + def add_extra_detail(self, extra_detail, *args): self.problem.add_extra_detail(extra_detail, *args) - + class Photo(RPDFile): - + title = _("photo") title_capitalized = _("Photo") - + def _assign_file_type(self): self.file_type = FILE_TYPE_PHOTO - + def load_metadata(self, temp_file=False): self.metadata = metadataphoto.MetaData(self._load_file_for_metadata(temp_file)) try: @@ -285,16 +298,16 @@ class Photo(RPDFile): return False else: return True - - + + class Video(RPDFile): - + title = _("video") title_capitalized = _("Video") - + def _assign_file_type(self): self.file_type = FILE_TYPE_VIDEO - + def load_metadata(self, temp_file=False): if self.extension == 'mts' or not metadatavideo.HAVE_HACHOIR: if metadatavideo.HAVE_HACHOIR: @@ -303,31 +316,33 @@ class Video(RPDFile): else: self.metadata = metadatavideo.VideoMetaData(self._load_file_for_metadata(temp_file)) return True - + class SamplePhoto(Photo): def __init__(self, sample_name='IMG_0524.CR2', sequences=None): Photo.__init__(self, name=sample_name, display_name=sample_name, path='/media/EOS_DIGITAL/DCIM/100EOS5D', - size=23516764, - file_system_modification_time=time.time(), + size=23516764, + file_system_modification_time=time.time(), scan_pid=2033, file_id='9873afe', - thm_full_name=None) + thm_full_name=None, + audio_file_full_name=None) self.sequences = sequences self.metadata = metadataphoto.DummyMetaData() self.download_start_time = datetime.datetime.now() - + class SampleVideo(Video): def __init__(self, sample_name='MVI_1379.MOV', sequences=None): Video.__init__(self, name=sample_name, display_name=sample_name, path='/media/EOS_DIGITAL/DCIM/100EOS5D', - size=823513764, - file_system_modification_time=time.time(), + size=823513764, + file_system_modification_time=time.time(), scan_pid=2033, file_id='9873qrsfe', - thm_full_name=None) + thm_full_name=None, + audio_file_full_name=None) self.sequences = sequences self.metadata = metadatavideo.DummyMetaData(filename=sample_name) self.download_start_time = datetime.datetime.now() diff --git a/rapid/scan.py b/rapid/scan.py index 637031a..f0e4422 100755 --- a/rapid/scan.py +++ b/rapid/scan.py @@ -42,59 +42,71 @@ file_attributes = "standard::name,standard::display-name,\ standard::type,standard::size,time::modified,access::can-read,id::file" -def get_video_THM_file(full_file_name_no_ext): - """ - Checks to see if a thumbnail file (THM) is in the same directory as the - file. Expects a full path to be part of the file name. - - Returns the filename, including path, if found, else returns None. - """ - +def get_associated_file(full_file_name_no_ext, extensions_to_check): + f = None - for e in rpdfile.VIDEO_THUMBNAIL_EXTENSIONS: + for e in extensions_to_check: if os.path.exists(full_file_name_no_ext + '.' + e): f = full_file_name_no_ext + '.' + e break if os.path.exists(full_file_name_no_ext + '.' + e.upper()): f = full_file_name_no_ext + '.' + e.upper() break - - return f + return f + +def get_audio_file(full_file_name_no_ext): + """ + Checks to see if an audio file of the same name is in the same directory + as the file. Expects a full path to be part of the file name. + + Returns the filename, including path, if found, else returns None. + """ + return get_associated_file(full_file_name_no_ext, rpdfile.AUDIO_EXTENSIONS) + +def get_video_THM_file(full_file_name_no_ext): + """ + Checks to see if a thumbnail file (THM) is in the same directory as the + file. Expects a full path to be part of the file name. + + Returns the filename, including path, if found, else returns None. + """ + + return get_associated_file(full_file_name_no_ext, rpdfile.VIDEO_THUMBNAIL_EXTENSIONS) class Scan(multiprocessing.Process): """Scans the given path for files of a specified type. - + Returns results in batches, finishing with a total of the size of all the files in bytes. """ - + def __init__(self, path, ignored_paths, use_re_ignored_paths, - batch_size, results_pipe, + batch_size, results_pipe, terminate_queue, run_event): - + """Setup values needed to conduct the scan. - + 'path' is a string of the path to be scanned, which is passed to gio. - + 'ignored_paths' is a list of paths that should not be scanned. Any path ending with one of the values will be ignored. - + 'use_re_ignored_paths': if true, pytho regular expressions will be used to determine which paths to ignore - - 'batch_size' is the number of files that should be sent back to the + + 'batch_size' is the number of files that should be sent back to the calling function at one time. - + 'results_pipe' is a connection on which to send the results. - - 'terminate_queue' is a queue whose sole purpose is to notify the + + 'terminate_queue' is a queue whose sole purpose is to notify the process that it should terminate and not return any results. - + 'run_event' is an Event that is used to temporarily halt execution. - + """ - + multiprocessing.Process.__init__(self) self.path = path self.ignored_paths = ignored_paths @@ -107,18 +119,18 @@ class Scan(multiprocessing.Process): self.files_scanned = 0 self.files = [] self.file_type_counter = rpdfile.FileTypeCounter() - + def _gio_scan(self, path, file_size_sum): """recursive function to scan a directory and its subdirectories for photos and possibly videos""" - + children = path.enumerate_children(file_attributes) - + for child in children: - + # pause if instructed by the caller self.run_event.wait() - + if not self.terminate_queue.empty(): x = self.terminate_queue.get() # terminate immediately @@ -128,26 +140,26 @@ class Scan(multiprocessing.Process): # only collect files and scan in directories we can actually read # cannot assume that users will download only from memory cards - + if child.get_attribute_boolean(gio.FILE_ATTRIBUTE_ACCESS_CAN_READ): file_type = child.get_file_type() name = child.get_name() if file_type == gio.FILE_TYPE_DIRECTORY: if not self.ignore_this_path(name): - file_size_sum = self._gio_scan(path.get_child(name), + file_size_sum = self._gio_scan(path.get_child(name), file_size_sum) if file_size_sum is None: return None elif file_type == gio.FILE_TYPE_REGULAR: - + self.files_scanned += 1 if self.files_scanned % 100 == 0: logger.debug("Scanned %s files", self.files_scanned) - + base_name, ext = os.path.splitext(name) ext = ext.lower()[1:] - + file_type = rpdfile.file_type(ext) if file_type is not None: file_id = child.get_attribute_string( @@ -161,31 +173,35 @@ class Scan(multiprocessing.Process): size = child.get_size() modification_time = child.get_modification_time() path_name = path.get_path() - + # look for thumbnail file for videos if file_type == rpdfile.FILE_TYPE_VIDEO: thm_full_name = get_video_THM_file(os.path.join(path_name, base_name)) else: thm_full_name = None - - scanned_file = rpdfile.get_rpdfile(ext, - name, - display_name, + + # check if an audio file is associated with the photo or video + audio_file_full_name = get_audio_file(os.path.join(path_name, base_name)) + + scanned_file = rpdfile.get_rpdfile(ext, + name, + display_name, path_name, size, modification_time, - thm_full_name, + thm_full_name, + audio_file_full_name, self.pid, file_id, file_type) - + self.files.append(scanned_file) file_size_sum += size - + if self.counter == self.batch_size: # send batch of results - self.results_pipe.send((rpdmp.CONN_PARTIAL, - (file_size_sum, + self.results_pipe.send((rpdmp.CONN_PARTIAL, + (file_size_sum, self.file_type_counter, self.pid, self.files))) @@ -193,14 +209,14 @@ class Scan(multiprocessing.Process): self.counter = 0 return file_size_sum - + def run(self): """start the actual scan.""" - + if self.use_re_ignored_paths and len(self.ignored_paths): self.re_pattern = prefsrapid.check_and_compile_re(self.ignored_paths) - + source = gio.File(self.path) try: if not self.ignore_this_path(self.path): @@ -210,25 +226,25 @@ class Scan(multiprocessing.Process): except gio.Error, inst: logger.error("Error while scanning %s: %s", self.path, inst) size = None - + if size is not None: if self.counter > 0: # send any remaining results - self.results_pipe.send((rpdmp.CONN_PARTIAL, (size, + self.results_pipe.send((rpdmp.CONN_PARTIAL, (size, self.file_type_counter, self.pid, self.files))) - - self.results_pipe.send((rpdmp.CONN_COMPLETE, (size, + + self.results_pipe.send((rpdmp.CONN_COMPLETE, (size, self.file_type_counter, self.pid))) - self.results_pipe.close() + self.results_pipe.close() def ignore_this_path(self, path): """ determines if the path should be ignored according to the preferences chosen by the user """ - + if len(self.ignored_paths): if self.use_re_ignored_paths and self.re_pattern: # regular expressions are being used @@ -238,5 +254,5 @@ class Scan(multiprocessing.Process): # regular expressions are not being used if path.endswith(tuple(self.ignored_paths)): return True - + return False diff --git a/rapid/subfolderfile.py b/rapid/subfolderfile.py index 4e23309..ec99a6a 100644 --- a/rapid/subfolderfile.py +++ b/rapid/subfolderfile.py @@ -268,6 +268,67 @@ class SubfolderFile(multiprocessing.Process): return rpd_file + def _move_associate_file(self, extension, full_base_name, temp_associate_file): + """Move (rename) the associate file using the pregenerated name + + Returns tuple of result (True if succeeded, False otherwise) and + full path and filename""" + + download_full_name = full_base_name + extension + + # move (rename) associate file + if self.use_gnome_file_operations: + source = gio.File(path=temp_associate_file) + dest = gio.File(path=download_full_name) + try: + source.move(dest, self.progress_callback_no_update, cancellable=None) + success = True + except gio.Error, inst: + success = False + else: + try: + # don't check to see if it already exists + shutil.move(temp_associate_file, download_full_name) + success = True + except: + success = False + + return (success, download_full_name) + + def move_thm_file(self, rpd_file): + """Move (rename) the THM thumbnail file using the pregenerated name""" + ext = None + if hasattr(rpd_file, 'thm_extension'): + if rpd_file.thm_extension: + ext = rpd_file.thm_extension + if ext is None: + ext = '.THM' + + result, rpd_file.download_thm_full_name = self._move_associate_file(ext, rpd_file.download_full_base_name, rpd_file.temp_thm_full_name) + + if not result: + logger.error("Failed to move video THM file %s", rpd_file.download_thm_full_name) + + return rpd_file + + def move_audio_file(self, rpd_file): + """Move (rename) the associate audio file using the pregenerated name""" + ext = None + if hasattr(rpd_file, 'audio_extension'): + if rpd_file.audio_extension: + ext = rpd_file.audio_extension + if ext is None: + ext = '.WAV' + + result, rpd_file.download_audio_full_name = self._move_associate_file(ext, rpd_file.download_full_base_name, rpd_file.temp_audio_full_name) + + if not result: + logger.error("Failed to move file's associated audio file %s", rpd_file.download_audio_full_name) + + return rpd_file + + + def run(self): """ Get subfolder and name. @@ -487,7 +548,7 @@ class SubfolderFile(multiprocessing.Process): else: rpd_file = self.download_failure_file_error(rpd_file, inst.strerror) except: - rpd_file = self.download_failure_file_error(rpd_file, inst.strerror) + rpd_file = self.download_failure_file_error(rpd_file, "An unknown error occurred while renaming the file") if add_unique_identifier: name = os.path.splitext(rpd_file.download_name) @@ -553,30 +614,10 @@ class SubfolderFile(multiprocessing.Process): self.downloads_today_date.value = self.downloads_today_tracker.get_raw_downloads_today_date() if rpd_file.temp_thm_full_name: - ext = None - if hasattr(rpd_file, 'thm_extension'): - if rpd_file.thm_extension: - ext = rpd_file.thm_extension - if ext is None: - ext = '.THM' - download_thm_full_name = rpd_file.download_full_base_name + ext - - # copy and rename THM video file - if self.use_gnome_file_operations: - source = gio.File(path=rpd_file.temp_thm_full_name) - dest = gio.File(path=download_thm_full_name) - try: - source.move(dest, self.progress_callback_no_update, cancellable=None) - rpd_file.download_thm_full_name = download_thm_full_name - except gio.Error, inst: - logger.error("Failed to move video THM file %s", download_thm_full_name) - else: - try: - # don't check to see if it already exists - shutil.move(rpd_file.temp_thm_full_name, download_thm_full_name) - rpd_file.download_thm_full_name = download_thm_full_name - except: - logger.error("Failed to move video THM file %s", download_thm_full_name) + rpd_file = self.move_thm_file(rpd_file) + + if rpd_file.temp_audio_full_name: + rpd_file = self.move_audio_file(rpd_file) if rpd_file.temp_xmp_full_name: # copy and rename XMP sidecar file |