From 05e7b52a1f94a40996f2619ad9db8bbdb1518497 Mon Sep 17 00:00:00 2001 From: Julien Valroff Date: Sun, 8 Jan 2012 07:54:46 +0100 Subject: Imported Upstream version 0.4.3 --- rapid/ChangeLog | 71 +++++ rapid/INSTALL | 2 + rapid/backupfile.py | 24 +- rapid/config.py | 2 +- rapid/copyfiles.py | 59 +++- rapid/filemodify.py | 138 +++++++++ rapid/generatename.py | 42 ++- rapid/generatenameconfig.py | 31 +- rapid/glade3/about.ui | 1 + rapid/glade3/prefs.ui | 689 +++++++------------------------------------- rapid/glade3/rapid.ui | 153 +++++++--- rapid/metadataexiftool.py | 225 +++++++++++++++ rapid/metadataphoto.py | 27 +- rapid/metadatavideo.py | 21 +- rapid/metadataxmp.py | 198 +++++++++++++ rapid/misc.py | 2 +- rapid/preferencesdialog.py | 42 +-- rapid/prefsrapid.py | 1 + rapid/rapid.py | 484 +++++++++++++++++++++++++++---- rapid/rpdfile.py | 92 +++--- rapid/scan.py | 38 ++- rapid/subfolderfile.py | 80 +++-- rapid/thumbnail.py | 70 ++--- 23 files changed, 1640 insertions(+), 852 deletions(-) create mode 100644 rapid/filemodify.py create mode 100755 rapid/metadataexiftool.py mode change 100755 => 100644 rapid/metadataphoto.py create mode 100755 rapid/metadataxmp.py (limited to 'rapid') diff --git a/rapid/ChangeLog b/rapid/ChangeLog index 35cd727..775d535 100644 --- a/rapid/ChangeLog +++ b/rapid/ChangeLog @@ -1,3 +1,74 @@ +Version 0.4.3 +------------- + +2012-01-07 + +ExifTool is now a required dependency for Rapid Photo Downloader. ExifTool +can be used to help download videos on Linux distributions that have not +packaged hachoir-metadata, such as Fedora. + +Exiftran is another new dependency. It is used to automatically rotate +JPEG images. + +Fixed bug #704482: Delete photos option should be easily accessible - + +Added a toolbar at the top of the main program window, which gives immediate +access to the most commonly changed configuration options: where files will +be transferred from, whether they will be copied or moved, and where they will +be transferred to. + +Please when the move option is chosen, all files in the download from a device +are first copied before any are deleted. In other words, only once all +source files have been successfully copied from a device to their destination +are the source files deleted from that device. + +Fixed bug #754531: extract Exif.CanonFi.FileNumber metadata - + +Added FileNumber metadata renaming option, which is a Canon-specific Exif value +in the form xxx-yyyy, where xxx is the folder number and yyyy is the image +number. Uses ExifTool. Thanks go to Etieene Charlier for researching the fix +and contributing code to get it implemented. + +Fixed bug #695517: Added functionality to download MTS video files. There is +currently no python based library to read metadata from MTS files, but ExifTool +works. + +Fixed bug #859998: Download THM video thumbnail files - + +Some video files have THM video thumbnail files associated with them. Rapid +Photo Downloader now downloads them and renames them to match the name of the +video it is associated with. + +Fixed bug #594533: Lossless JPEG rotation based on EXIF data after picture +transfer - + +There is now an option to automatically rotate JPEG photos as they are +downloaded. The program exiftran is used to do the rotation. The feature is +turned on default. + +Fixed bug #859012: Confirm if really want to download from /home, /media or / - + +It is possible for the program's preferences to be set to download from /home, +/media or / (the root of the file system). This can result in the program +scanning a very large number of files, possibly causing the system to become +unresponsive. The program now queries the user before commencing this scan to +confirm if this is really what they want to do. + +Fixed bug #792228: clear all thumbnails when refresh command issued. + +Fixed a bug where the device progress bar would occasionally disappear when +the download device was changed. + +Fixed a bug where the file extensions the program downloads could not be +displayed from the command line. + +Fixed a bug where the program would crash when trying to convert a malformed +thumbnail from one image mode to another. + +Updated Czech, Danish, Dutch, French, German, Hungarian, Italian, Norwegian, +Polish, Serbian, Slovak, Spanish and Swedish translations. + + Version 0.4.2 ------------- diff --git a/rapid/INSTALL b/rapid/INSTALL index 8cde587..dd0ac2b 100644 --- a/rapid/INSTALL +++ b/rapid/INSTALL @@ -8,6 +8,8 @@ Rapid Photo Downloader requires the following software: * python-notify 0.1.1 or higher * python-imaging 1.1.7 or higher * librsvg2-common 2.26 or higher +* exiftool +* exiftran To run Rapid Photo Downloader you will need all the software mentioned above. diff --git a/rapid/backupfile.py b/rapid/backupfile.py index 40039bc..39efb53 100644 --- a/rapid/backupfile.py +++ b/rapid/backupfile.py @@ -85,7 +85,21 @@ class BackupFiles(multiprocessing.Process): def progress_callback(self, amount_downloaded, total): self.update_progress(amount_downloaded, total) - + def progress_callback_no_update(self, amount_downloaded, total): + """called when copying very small files""" + pass + + def backup_additional_file(self, dest_dir, full_file_name): + """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) + def run(self): self.cancel_copy = gio.Cancellable() @@ -174,6 +188,14 @@ class BackupFiles(multiprocessing.Process): rpd_file.status = config.STATUS_DOWNLOAD_AND_BACKUP_FAILED else: rpd_file.status = config.STATUS_BACKUP_PROBLEM + else: + # backup any THM 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_xmp_full_name: + self.backup_additional_file(dest_dir, + rpd_file.download_xmp_full_name) self.total_downloaded += rpd_file.size bytes_not_downloaded = rpd_file.size - self.amount_downloaded diff --git a/rapid/config.py b/rapid/config.py index 47ecfff..74802ec 100644 --- a/rapid/config.py +++ b/rapid/config.py @@ -15,7 +15,7 @@ ### along with this program; if not, write to the Free Software ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -version = '0.4.2' +version = '0.4.3' GCONF_KEY="/apps/rapid-photo-downloader" diff --git a/rapid/copyfiles.py b/rapid/copyfiles.py index 65a6c6d..f6ff073 100644 --- a/rapid/copyfiles.py +++ b/rapid/copyfiles.py @@ -20,6 +20,8 @@ import multiprocessing import tempfile import os +import random +import string import gio @@ -37,7 +39,11 @@ from gettext import gettext as _ class CopyFiles(multiprocessing.Process): + """ + Copies files from source to temporary directory, giving them a random name + """ 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, run_event): @@ -48,6 +54,8 @@ class CopyFiles(multiprocessing.Process): self.photo_download_folder = photo_download_folder self.video_download_folder = video_download_folder self.files = files + self.modify_files_during_download = modify_files_during_download + self.modify_pipe = modify_pipe self.scan_pid = scan_pid self.no_files= len(self.files) self.run_event = run_event @@ -85,10 +93,17 @@ class CopyFiles(multiprocessing.Process): 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 @@ -107,8 +122,8 @@ class CopyFiles(multiprocessing.Process): if self.photo_temp_dir or self.video_temp_dir: self.thumbnail_maker = tn.Thumbnail() - - for i in range(len(self.files)): + + for i in range(self.no_files): rpd_file = self.files[i] self.total_reached = False @@ -119,9 +134,13 @@ class CopyFiles(multiprocessing.Process): 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), - rpd_file.name) + temp_name) rpd_file.temp_full_file_name = temp_full_file_name dest = gio.File(path=temp_full_file_name) @@ -152,9 +171,26 @@ class CopyFiles(multiprocessing.Process): # 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) + # reuse video's file name + temp_thm_full_name = temp_full_file_name + '__rpd__thm' + dest = gio.File(path=temp_thm_full_name) + try: + source.copy(dest, self.thm_progress_callback, cancellable=self.cancel_copy) + rpd_file.temp_thm_full_name = temp_thm_full_name + logger.debug("Copied video THM file %s", rpd_file.temp_thm_full_name) + except gio.Error, inst: + logger.error("Failed to download video THM file: %s", rpd_file.thm_full_name) + else: + temp_thm_full_name = None + + if copy_succeeded and rpd_file.generate_thumbnail: thumbnail, thumbnail_icon = self.thumbnail_maker.get_thumbnail( temp_full_file_name, + temp_thm_full_name, rpd_file.file_type, (160, 120), (100,100)) else: @@ -163,10 +199,19 @@ class CopyFiles(multiprocessing.Process): if rpd_file.metadata is not None: rpd_file.metadata = None - - self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_FILE, - (copy_succeeded, rpd_file, i + 1, temp_full_file_name, - thumbnail_icon, thumbnail)))) + + + 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, + thumbnail_icon, thumbnail, copy_finished)) + else: + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_FILE, + (copy_succeeded, rpd_file, download_count, + temp_full_file_name, + thumbnail_icon, thumbnail)))) self.results_pipe.send((rpdmp.CONN_COMPLETE, None)) diff --git a/rapid/filemodify.py b/rapid/filemodify.py new file mode 100644 index 0000000..3f4f2be --- /dev/null +++ b/rapid/filemodify.py @@ -0,0 +1,138 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch + +### This program 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 2 of the License, or +### (at your option) any later version. + +### This program 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 this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os.path, fractions +import subprocess +import multiprocessing +import logging +logger = multiprocessing.get_logger() + +import rpdmultiprocessing as rpdmp +import rpdfile +import metadataxmp as mxmp +import subfolderfile + +WRITE_XMP_INPLACE = rpdfile.NON_RAW_IMAGE_EXTENSIONS + ['dng'] + +def lossless_rotate(jpeg): + """using exiftran, performs a lossless, inplace translation of a jpeg, preserving time stamps""" + try: + logger.debug("Auto rotating %s", jpeg) + proc = subprocess.Popen(['exiftran', '-a', '-i', '-p', jpeg], stdout=subprocess.PIPE) + v = proc.communicate()[0].strip() + except OSError: + v = None + return v + +class FileModify(multiprocessing.Process): + def __init__(self, auto_rotate_jpeg, focal_length, results_pipe, terminate_queue, + run_event): + multiprocessing.Process.__init__(self) + self.results_pipe = results_pipe + self.terminate_queue = terminate_queue + self.run_event = run_event + + self.auto_rotate_jpeg = auto_rotate_jpeg + self.focal_length = focal_length + + def check_termination_request(self): + """ + Check to see this process has not been requested to immediately terminate + """ + if not self.terminate_queue.empty(): + x = self.terminate_queue.get() + # terminate immediately + return True + return False + + def create_rational(self, value): + return '%s/%s' % (value.numerator, value.denominator) + + def run(self): + + download_count = 0 + copy_finished = False + while not copy_finished: + logger.debug("Finished %s. Getting next task.", download_count) + + rpd_file, download_count, temp_full_file_name, thumbnail_icon, thumbnail, copy_finished = self.results_pipe.recv() + if rpd_file is None: + # this is a termination signal + logger.info("Terminating file modify via pipe") + return None + # pause if instructed by the caller + self.run_event.wait() + + if self.check_termination_request(): + return None + + if self.auto_rotate_jpeg and rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: + if rpd_file.extension in rpdfile.JPEG_EXTENSIONS: + lossless_rotate(rpd_file.temp_full_file_name) + + xmp_sidecar = None + # check to see if focal length and aperture data should be manipulated + if self.focal_length is not None and rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: + if subfolderfile.load_metadata(rpd_file, temp_file=True): + a = rpd_file.metadata.aperture() + if a == '0.0': + logger.info("Adjusting focal length and aperture for %s (%s)", rpd_file.temp_full_file_name, rpd_file.name) + + new_focal_length = fractions.Fraction(self.focal_length,1) + new_aperture = fractions.Fraction(8,1) + if rpd_file.extension in WRITE_XMP_INPLACE: + try: + rpd_file.metadata["Exif.Photo.FocalLength"] = new_focal_length + rpd_file.metadata["Exif.Photo.FNumber"] = new_aperture + rpd_file.metadata.write(preserve_timestamps=True) + logger.debug("Wrote new focal length and aperture to %s (%s)", rpd_file.temp_full_file_name, rpd_file.name) + except: + logger.error("failed to write new focal length and aperture to %s (%s)!", rpd_file.temp_full_file_name, rpd_file.name) + else: + # write to xmp sidecar + xmp_sidecar = mxmp.XmpMetadataSidecar(rpd_file.temp_full_file_name) + xmp_sidecar.set_exif_value('FocalLength', self.create_rational(new_focal_length)) + xmp_sidecar.set_exif_value('FNumber', self.create_rational(new_aperture)) + # store values in rpd_file, so they can be used in the subfolderfile process + rpd_file.new_focal_length = new_focal_length + rpd_file.new_aperture = new_aperture + + if False: + xmp_sidecar.set_contact_url('http://www.website.net') + xmp_sidecar.set_contact_email('user@email.com') + + if xmp_sidecar is not None: + # need to write out xmp sidecar + o = xmp_sidecar.write_xmp_sidecar() + logger.debug("Wrote XMP sidecar file") + logger.debug("exiv2 output: %s", o) + rpd_file.temp_xmp_full_name = rpd_file.temp_full_file_name + '.xmp' + + + copy_succeeded = True + rpd_file.metadata = None #purge metadata, as it cannot be pickled + + self.results_pipe.send((rpdmp.CONN_PARTIAL, + (copy_succeeded, rpd_file, download_count, + temp_full_file_name, + thumbnail_icon, thumbnail))) + + self.results_pipe.send((rpdmp.CONN_COMPLETE, None)) + + diff --git a/rapid/generatename.py b/rapid/generatename.py index 1c95478..99d7287 100644 --- a/rapid/generatename.py +++ b/rapid/generatename.py @@ -112,6 +112,39 @@ 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): + """ + Generates THM extension with correct capitalization, if needed + """ + if self.rpd_file.thm_full_name: + thm_extension = os.path.splitext(self.rpd_file.thm_full_name)[1] + if self.L2 == UPPERCASE: + thm_extension = thm_extension.upper() + elif self.L2 == LOWERCASE: + thm_extension = thm_extension.lower() + self.rpd_file.thm_extension = thm_extension + else: + self.rpd_file.thm_extension = None + + def _get_xmp_extension(self, extension): + """ + Generates XMP extension with correct capitalization, if needed. + """ + if self.rpd_file.temp_xmp_full_name: + if self.L2 == UPPERCASE: + self.rpd_file.xmp_extension = '.XMP' + elif self.L2 == LOWERCASE: + self.rpd_file.xmp_extension = '.xmp' + else: + # mimic capitalization of extension + if extension.isupper(): + self.rpd_file.xmp_extension = '.XMP' + else: + self.rpd_file.xmp_extension = '.xmp' + else: + self.rpd_file.xmp_extension = None + + def _get_filename_component(self): """ Returns portion of new file / subfolder name based on the file name @@ -121,9 +154,13 @@ class PhotoName: if self.L1 == NAME_EXTENSION: filename = self.rpd_file.name + self._get_thm_extension() + self._get_xmp_extension(extension) elif self.L1 == NAME: filename = name elif self.L1 == EXTENSION: + self._get_thm_extension() + self._get_xmp_extension(extension) if extension: if not self.strip_initial_period_from_extension: # keep the period / dot of the extension, so the user does not @@ -196,7 +233,10 @@ class PhotoName: padding = LIST_SHUTTER_COUNT_L2.index(self.L2) + 3 formatter = '%0' + str(padding) + "i" v = formatter % v - + elif self.L1 == FILE_NUMBER: + v = self.rpd_file.metadata.file_number() + if v and self.L2 == FILE_NUMBER_FOLDER: + v = v[:3] elif self.L1 == OWNER_NAME: v = self.rpd_file.metadata.owner_name() elif self.L1 == ARTIST: diff --git a/rapid/generatenameconfig.py b/rapid/generatenameconfig.py index 4d1f75c..0fbeb38 100644 --- a/rapid/generatenameconfig.py +++ b/rapid/generatenameconfig.py @@ -31,7 +31,7 @@ ORDER_KEY = "__order__" # is to have them put into the translation template. If you change the values below # then you MUST change the value in class i18TranslateMeThanks as well!! -# *** Level 0 +# *** Level 0, i.e. first column of values presented to user DATE_TIME = 'Date time' TEXT = 'Text' FILENAME = 'Filename' @@ -41,7 +41,7 @@ JOB_CODE = 'Job code' SEPARATOR = os.sep -# *** Level 1 +# *** Level 1, i.e. second column of values presented to user # Date time IMAGE_DATE = 'Image date' @@ -68,6 +68,9 @@ SHORT_CAMERA_MODEL = 'Short camera model' SHORT_CAMERA_MODEL_HYPHEN = 'Hyphenated short camera model' SERIAL_NUMBER = 'Serial number' SHUTTER_COUNT = 'Shutter count' +#Currently the only file number is Exif.CanonFi.FileNumber, +#which is in the format xxx-yyyy, where xxx is the folder and yyyy the image +FILE_NUMBER = 'File number' OWNER_NAME = 'Owner name' COPYRIGHT = 'Copyright' ARTIST = 'Artist' @@ -84,12 +87,11 @@ DOWNLOAD_SEQ_NUMBER = 'Downloads today' SESSION_SEQ_NUMBER = 'Session number' SUBFOLDER_SEQ_NUMBER = 'Subfolder number' STORED_SEQ_NUMBER = 'Stored number' - SEQUENCE_LETTER = 'Sequence letter' -# *** Level 2 +# *** Level 2, i.e. third and final column of values presented to user # Image number IMAGE_NUMBER_ALL = 'All digits' @@ -113,6 +115,9 @@ SEQUENCE_NUMBER_5 = "Five digits" SEQUENCE_NUMBER_6 = "Six digits" SEQUENCE_NUMBER_7 = "Seven digits" +#File number +FILE_NUMBER_FOLDER = "Folder only" +FILE_NUMBER_ALL = "Folder and file" # Now, define dictionaries and lists of valid combinations of preferences. @@ -184,6 +189,12 @@ class i18TranslateMeThanks: _('Serial number') # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata _('Shutter count') + #File number currently refers to the Exif value Exif.Canon.FileNumber + _('File number') + #Only the folder component of the Exif.Canon.FileNumber value + _('Folder only') + #The folder and file component of the Exif.Canon.FileNumber value + _('Folder and file') # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata _('Owner name') _('Codec') @@ -310,8 +321,12 @@ LIST_SHUTTER_COUNT_L2 = [ SEQUENCE_NUMBER_3, SEQUENCE_NUMBER_4, SEQUENCE_NUMBER_5, - SEQUENCE_NUMBER_6, + SEQUENCE_NUMBER_6, ] +FILE_NUMBER_L2 = [ + FILE_NUMBER_FOLDER, + FILE_NUMBER_ALL + ] # Level 1 LIST_DATE_TIME_L1 = [IMAGE_DATE, TODAY, YESTERDAY, DOWNLOAD_TIME] @@ -367,7 +382,8 @@ LIST_METADATA_L1 = [APERTURE, ISO, EXPOSURE_TIME, FOCAL_LENGTH, SHORT_CAMERA_MODEL, SHORT_CAMERA_MODEL_HYPHEN, SERIAL_NUMBER, - SHUTTER_COUNT, + SHUTTER_COUNT, + FILE_NUMBER, OWNER_NAME, ARTIST, COPYRIGHT] @@ -384,7 +400,8 @@ DICT_METADATA_L1 = { SHORT_CAMERA_MODEL: LIST_CASE_L2, SHORT_CAMERA_MODEL_HYPHEN: LIST_CASE_L2, SERIAL_NUMBER: None, - SHUTTER_COUNT: LIST_SHUTTER_COUNT_L2, + SHUTTER_COUNT: LIST_SHUTTER_COUNT_L2, + FILE_NUMBER: FILE_NUMBER_L2, OWNER_NAME: LIST_CASE_L2, ARTIST: LIST_CASE_L2, COPYRIGHT: LIST_CASE_L2, diff --git a/rapid/glade3/about.ui b/rapid/glade3/about.ui index 3e59698..7e10e37 100644 --- a/rapid/glade3/about.ui +++ b/rapid/glade3/about.ui @@ -55,6 +55,7 @@ Aron Xu <happyaron.xu@gmail.com> True False + vertical 2 diff --git a/rapid/glade3/prefs.ui b/rapid/glade3/prefs.ui index 0a13978..2e0e53c 100644 --- a/rapid/glade3/prefs.ui +++ b/rapid/glade3/prefs.ui @@ -1,7 +1,7 @@ - + - + 23 1 @@ -13,7 +13,6 @@ 10 - False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 Preferences: Rapid Photo Downloader @@ -22,69 +21,21 @@ 500 rapid-photo-downloader.svg dialog - - + + True - False 2 - - - True - False - end - - - gtk-help - True - True - True - False - True - - - False - False - 0 - True - - - - - gtk-close - True - True - True - False - True - - - False - False - 1 - - - - - False - True - end - 0 - - True - False 2 True True queue - automatic - automatic in @@ -93,13 +44,11 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK False - + - True - True 5 0 @@ -112,33 +61,27 @@ True - False 12 True - False True - False 6 True - False gtk-directory False - True 0 True - False <span weight="bold" size="x-large">Photo Download Folders</span> True @@ -150,19 +93,15 @@ - True - True 0 True - False False - True 1 @@ -176,29 +115,24 @@ True - False 12 True - False False - True 0 True - False 7 3 True - False 0 <i>Example: /home/user/Pictures</i> True @@ -214,7 +148,6 @@ True - False 0 <b>Download Subfolders</b> True @@ -229,7 +162,6 @@ True - False 0 Download folder: @@ -245,7 +177,6 @@ True - False 0 12 Choose the download folder. Subfolders for the downloaded photos will be automatically created in this folder using the structure specified below. @@ -263,7 +194,6 @@ True - False 0 <b>Download Folder</b> True @@ -276,7 +206,6 @@ True - False 0 0 True @@ -293,7 +222,6 @@ True - False @@ -326,26 +254,20 @@ - True - True 1 True - False False - True 2 - True - True 12 1 @@ -358,7 +280,6 @@ True - False Photo Folders @@ -368,57 +289,46 @@ True - False 12 True - False True - False 6 True - False gtk-convert False - True 0 True - False <span weight="bold" size="x-large">Photo Rename</span> True False - True 1 - True - True 0 True - False False - True 1 @@ -432,34 +342,28 @@ True - False 12 True - False False - True 0 True - False 12 - False 0 <b>Photo Rename</b> True False - True 0 @@ -467,19 +371,15 @@ True True - automatic - automatic True True - False queue none True - False @@ -489,21 +389,17 @@ - True - True 1 True - False 3 3 True - False @@ -515,7 +411,6 @@ True - False @@ -528,7 +423,6 @@ True - False 0 0 translators please ignore this @@ -545,7 +439,6 @@ True - False 0 translators please ignore this @@ -560,7 +453,6 @@ True - False 0 0 <i>New:</i> @@ -578,7 +470,6 @@ True - False 0 <i>Original:</i> True @@ -595,7 +486,6 @@ True - False 0 <b>Example</b> True @@ -614,26 +504,20 @@ - True - True 1 True - False False - True 2 - True - True 12 1 @@ -647,7 +531,6 @@ True - False Photo Rename @@ -658,33 +541,27 @@ True - False 12 True - False True - False 6 True - False gtk-directory False - True 0 True - False <span weight="bold" size="x-large">Video Download Folders</span> True @@ -696,19 +573,15 @@ - True - True 0 True - False False - True 1 @@ -721,20 +594,16 @@ - False - False 0 12 10 - Sorry, video downloading functionality disabled. To download videos, please install the <i>hachoir metadata</i> and <i>kaa metadata</i> packages for python. + Sorry, video downloading functionality disabled. To download videos, please install either the <i>hachoir metadata</i> and <i>kaa metadata</i> packages for python, or <i>exiftool</i>. True True - True - True 0 @@ -748,29 +617,24 @@ True - False 12 True - False False - True 0 True - False 7 3 True - False 0 Download folder: @@ -786,7 +650,6 @@ True - False 0 12 Choose the download folder. Subfolders for the downloaded videos will be automatically created in this folder using the structure specified below. @@ -804,7 +667,6 @@ True - False 0 <b>Download Folder</b> True @@ -817,7 +679,6 @@ True - False 0 <b>Download Subfolders</b> True @@ -832,7 +693,6 @@ True - False 0 <i>Example: /home/user/Pictures</i> True @@ -848,7 +708,6 @@ True - False 0 0 True @@ -865,7 +724,6 @@ True - False @@ -898,26 +756,20 @@ - True - True 1 True - False False - True 2 - True - True 12 2 @@ -930,7 +782,6 @@ True - False Video Folders @@ -941,57 +792,46 @@ True - False 12 True - False True - False 6 True - False gtk-convert False - True 0 True - False <span weight="bold" size="x-large">Video Rename</span> True False - True 1 - True - True 0 True - False False - True 1 @@ -1005,29 +845,24 @@ True - False 12 True - False False - True 0 True - False 12 - False 0 - Sorry, video downloading functionality disabled. To download videos, please install the <i>hachoir metadata</i> and <i>kaa metadata</i> packages for python. + Sorry, video downloading functionality disabled. To download videos, please install either the <i>hachoir metadata</i> and <i>kaa metadata</i> packages for python, or <i>exiftool</i>. True True @@ -1041,19 +876,15 @@ True True - automatic - automatic True True - False queue none True - False @@ -1063,21 +894,17 @@ - True - True 1 True - False 3 3 True - False @@ -1089,7 +916,6 @@ True - False @@ -1102,7 +928,6 @@ True - False 0 0 translators please ignore this @@ -1119,7 +944,6 @@ True - False 0 translators please ignore this @@ -1134,7 +958,6 @@ True - False 0 0 <i>New:</i> @@ -1152,7 +975,6 @@ True - False 0 <i>Original:</i> True @@ -1169,7 +991,6 @@ True - False 0 <b>Example</b> True @@ -1188,26 +1009,20 @@ - True - True 1 True - False False - True 2 - True - True 12 1 @@ -1220,7 +1035,6 @@ True - False Video Rename @@ -1231,57 +1045,46 @@ True - False 12 True - False True - False 6 True - False input-keyboard False - True 0 True - False <span weight="bold" size="x-large">Rename Options</span> True False - True 1 - True - True 0 True - False False - True 1 @@ -1295,11 +1098,9 @@ True - False True - False 0 12 <b>Sequence Numbers</b> @@ -1307,140 +1108,112 @@ False - True 0 True - False 12 True - False 12 False - True 0 True - False 12 True - False 0 Specify the time in 24 hour format at which the <i>Downloads today</i> sequence number should be reset. True True - True - True 0 True - False True - False 6 True - False 0 Day start: - True - True 0 True - False 0 Downloads today: - True - True 1 True - False 0 Stored number: - True - True 2 False - True 0 True - False 6 False - True 1 True - False 6 True - False True True 2 - • + 2 1 True - False - False - True - True hour_adjustment True - + False @@ -1451,7 +1224,6 @@ True - False : @@ -1465,17 +1237,13 @@ True True 2 - • + 2 1 True - False - False - True - True minute_adjustment True - + False @@ -1486,20 +1254,17 @@ True - False 0 hh:mm False - True 3 False - True 0 @@ -1512,14 +1277,11 @@ False - True 2 - True - True 1 @@ -1529,38 +1291,30 @@ True True False - False True - + - True - True 2 - True - True 1 True - False False - True 2 False - True 12 1 @@ -1568,7 +1322,6 @@ True - False 0 12 <b>Compatibility with Other Operating Systems</b> @@ -1576,37 +1329,31 @@ False - True 3 True - False 12 True - False 12 False - True 0 True - False 2 2 True - False 0 Specify whether photo, video and folder names should have any characters removed that are not allowed by other operating systems. True @@ -1621,10 +1368,9 @@ True True False - False True True - + 2 @@ -1634,34 +1380,27 @@ - True - True 1 True - False False - True 2 False - True 12 4 - True - True 12 1 @@ -1674,7 +1413,6 @@ True - False Rename Options @@ -1685,57 +1423,46 @@ True - False 12 True - False True - False 6 True - False rapid-photo-downloader-jobcode False - True 0 True - False <span weight="bold" size="x-large">Job Codes</span> True False - True 1 - True - True 0 True - False False - True 1 @@ -1749,14 +1476,11 @@ True - False True - False - False 0 12 <b>Job Codes</b> @@ -1764,23 +1488,19 @@ False - True 0 True - False 12 True - False False - True 0 @@ -1788,8 +1508,6 @@ True True - automatic - automatic in @@ -1803,18 +1521,15 @@ False - True 1 True - False True - False 12 start @@ -1823,9 +1538,8 @@ True True True - False True - + False @@ -1839,9 +1553,8 @@ True True True - False True - + False @@ -1855,9 +1568,8 @@ True True True - False True - + False @@ -1867,22 +1579,17 @@ - True - True 0 False - True 2 - True - True 1 @@ -1891,15 +1598,11 @@ - True - True 0 - True - True 1 @@ -1911,7 +1614,6 @@ True - False Job Codes @@ -1922,57 +1624,46 @@ True - False 12 True - False True - False 6 True - False - media-flash + drive-removable-media False - True 0 True - False <span weight="bold" size="x-large">Devices</span> True False - True 1 - True - True 0 True - False False - True 1 @@ -1986,11 +1677,9 @@ True - False 12 - False 0 12 Devices @@ -2000,14 +1689,12 @@ False - True 0 True - False 0 12 Devices are from where to download photos and videos, such as cameras, memory cards or Portable Storage Devices. @@ -2019,31 +1706,25 @@ You can download from multiple devices simultaneously, or you can specify a loca True - True - True 1 True - False True - False 3 False - True 0 True - False 3 2 3 @@ -2053,9 +1734,8 @@ You can download from multiple devices simultaneously, or you can specify a loca True True False - False True - + 1 @@ -2071,9 +1751,8 @@ You can download from multiple devices simultaneously, or you can specify a loca True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - False True - + 2 @@ -2082,7 +1761,6 @@ You can download from multiple devices simultaneously, or you can specify a loca True - False 0 6 If you enable automatic detection of Portable Storage Devices, the entire device will be scanned. On large devices, this could take some time. @@ -2105,44 +1783,36 @@ When this option is enabled, and a potential device is detected, you will be pro - True - True 1 True - False False - True 2 False - True 2 True - False 12 True - False 2 2 True - False 0 12 Location: @@ -2157,7 +1827,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 12 @@ -2174,8 +1843,6 @@ When this option is enabled, and a potential device is detected, you will be pro - True - True 0 @@ -2185,25 +1852,21 @@ When this option is enabled, and a potential device is detected, you will be pro True - False False - True 2 False - True 3 False - True 12 1 @@ -2216,7 +1879,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False Devices @@ -2227,69 +1889,57 @@ When this option is enabled, and a potential device is detected, you will be pro True - False True - False 6 True - False gtk-preferences False - True 0 True - False <span weight="bold" size="x-large">Device Options</span> True False - True 1 False - True 0 True - False False - True 1 True - False 12 True - False 12 True - False 0 12 Remembered Paths @@ -2299,14 +1949,12 @@ When this option is enabled, and a potential device is detected, you will be pro False - True 0 True - False 0 12 Remembered paths are those associated with devices that you have chosen to always scan or ignore when automatic detection of Portable Storage Devices is enabled. @@ -2314,23 +1962,19 @@ When this option is enabled, and a potential device is detected, you will be pro False - True 1 True - False 12 True - False False - True 0 @@ -2338,8 +1982,6 @@ When this option is enabled, and a potential device is detected, you will be pro True True - automatic - automatic 250 @@ -2351,18 +1993,15 @@ When this option is enabled, and a potential device is detected, you will be pro False - True 1 True - False True - False 12 start @@ -2371,9 +2010,8 @@ When this option is enabled, and a potential device is detected, you will be pro True True True - False True - + False @@ -2387,9 +2025,8 @@ When this option is enabled, and a potential device is detected, you will be pro True True True - False True - + False @@ -2399,29 +2036,22 @@ When this option is enabled, and a potential device is detected, you will be pro - True - True 0 False - True 2 - True - True 2 - True - True 12 0 @@ -2429,12 +2059,10 @@ When this option is enabled, and a potential device is detected, you will be pro True - False 12 True - False 0 12 Ignored Paths @@ -2444,14 +2072,12 @@ When this option is enabled, and a potential device is detected, you will be pro False - True 0 True - False 0 12 Specify the ending portion of any paths you want ignored when scanning devices for photos or videos. Any path ending with the values below will not be scanned. @@ -2459,23 +2085,19 @@ When this option is enabled, and a potential device is detected, you will be pro False - True 1 True - False 12 True - False False - True 0 @@ -2483,8 +2105,6 @@ When this option is enabled, and a potential device is detected, you will be pro True True - automatic - automatic 250 @@ -2496,18 +2116,15 @@ When this option is enabled, and a potential device is detected, you will be pro False - True 1 True - False True - False 12 start @@ -2516,9 +2133,8 @@ When this option is enabled, and a potential device is detected, you will be pro True True True - False True - + False @@ -2532,9 +2148,8 @@ When this option is enabled, and a potential device is detected, you will be pro True True True - False True - + False @@ -2548,9 +2163,8 @@ When this option is enabled, and a potential device is detected, you will be pro True True True - False True - + False @@ -2560,43 +2174,35 @@ When this option is enabled, and a potential device is detected, you will be pro - True - True 0 False - True 2 - True - True 2 True - False Use _python-style regular expressions True True False - False True True - + False - True 12 0 @@ -2604,21 +2210,16 @@ When this option is enabled, and a potential device is detected, you will be pro False - True 3 - True - True 1 - True - True 2 @@ -2630,7 +2231,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False Device Options @@ -2641,56 +2241,45 @@ When this option is enabled, and a potential device is detected, you will be pro True - False True - False True - False 6 True - False drive-removable-media False - True 0 True - False <span weight="bold" size="x-large">Backup</span> True False - True 1 - True - True 0 True - False False - True 1 @@ -2704,10 +2293,8 @@ When this option is enabled, and a potential device is detected, you will be pro True - False - False 0 12 <b>Backup</b> @@ -2715,36 +2302,30 @@ When this option is enabled, and a potential device is detected, you will be pro False - True 0 True - False 12 True - False False - True 0 True - False 10 4 True - False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 12 @@ -2766,9 +2347,8 @@ When this option is enabled, and a potential device is detected, you will be pro True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - False True - + 1 @@ -2781,7 +2361,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False 0 12 You can have your photos and videos backed up to multiple locations as they are downloaded, e.g. external hard drives. @@ -2798,10 +2377,9 @@ When this option is enabled, and a potential device is detected, you will be pro True True False - False True True - + 4 @@ -2813,7 +2391,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False 0 6 Specify the folder in which backups are stored on the device. @@ -2832,7 +2409,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False 0 Photo backup location: @@ -2848,7 +2424,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False 0 Photo backup folder name: @@ -2865,7 +2440,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False 0 0 6 @@ -2883,7 +2457,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False 0 0 6 @@ -2902,12 +2475,8 @@ When this option is enabled, and a potential device is detected, you will be pro True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - • - False - False - True - True - + + 3 @@ -2921,7 +2490,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False 0 Video backup folder name: @@ -2940,12 +2508,8 @@ When this option is enabled, and a potential device is detected, you will be pro True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - • - False - False - True - True - + + 3 @@ -2959,7 +2523,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False 0 Video backup location: @@ -3016,33 +2579,26 @@ When this option is enabled, and a potential device is detected, you will be pro - True - True 1 True - False False - True 2 False - True 1 - True - True 12 1 @@ -3055,7 +2611,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False Backup @@ -3066,56 +2621,45 @@ When this option is enabled, and a potential device is detected, you will be pro True - False True - False True - False 6 True - False gtk-execute False - True 0 True - False <span weight="bold" size="x-large">Miscellaneous</span> True False - True 1 - True - True 0 True - False False - True 1 @@ -3129,11 +2673,9 @@ When this option is enabled, and a potential device is detected, you will be pro True - False True - False 0 12 <b>Program Automation</b> @@ -3141,30 +2683,24 @@ When this option is enabled, and a potential device is detected, you will be pro False - True 0 True - False 6 - - False - + False - True 0 True - False - 7 + 6 3 @@ -3173,9 +2709,9 @@ When this option is enabled, and a potential device is detected, you will be pro True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - False + 0 True - + 3 @@ -3190,9 +2726,9 @@ When this option is enabled, and a potential device is detected, you will be pro True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - False + 0 True - + 3 @@ -3205,9 +2741,9 @@ When this option is enabled, and a potential device is detected, you will be pro True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - False + 0 True - + 3 @@ -3221,9 +2757,9 @@ When this option is enabled, and a potential device is detected, you will be pro True True False - False + 0 True - + 3 @@ -3232,36 +2768,36 @@ When this option is enabled, and a potential device is detected, you will be pro - - Delete photos and videos from device upon download completion + + Exit program even if download had warnings or errors True True False - False + 0 True - + - 3 - 5 - 6 + 1 + 2 + 4 + 5 - - Exit program even if download had warnings or errors + + Automatically rotate JPEG images True True False - False + 0 True - + - 1 - 2 - 4 - 5 + 3 + 5 + 6 @@ -3270,19 +2806,8 @@ When this option is enabled, and a potential device is detected, you will be pro - - - - - - - - - - True - True 24 1 @@ -3290,18 +2815,15 @@ When this option is enabled, and a potential device is detected, you will be pro True - False False - True 2 False - True 12 1 @@ -3309,7 +2831,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False 0 12 Performance @@ -3319,29 +2840,23 @@ When this option is enabled, and a potential device is detected, you will be pro False - True 2 True - False 6 - - False - + False - True 0 True - False 3 @@ -3349,9 +2864,8 @@ When this option is enabled, and a potential device is detected, you will be pro True True False - False True - + @@ -3362,8 +2876,6 @@ When this option is enabled, and a potential device is detected, you will be pro - True - True 24 1 @@ -3371,26 +2883,21 @@ When this option is enabled, and a potential device is detected, you will be pro True - False False - True 2 False - True 12 3 - True - True 12 1 @@ -3403,7 +2910,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Miscellaneous @@ -3415,57 +2921,46 @@ When this option is enabled, and a potential device is detected, you will be pro True - False 12 True - False True - False 6 True - False gtk-dialog-error False - True 0 True - False <span weight="bold" size="x-large">Error Handling</span> True False - True 1 - True - True 0 True - False False - True 1 @@ -3479,29 +2974,24 @@ When this option is enabled, and a potential device is detected, you will be pro True - False 12 True - False False - True 0 True - False 14 2 True - False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -3515,7 +3005,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False 0 <b>Photo and Video Name Conflicts</b> True @@ -3531,11 +3020,10 @@ When this option is enabled, and a potential device is detected, you will be pro True True False - False True True True - + 1 @@ -3551,7 +3039,6 @@ When this option is enabled, and a potential device is detected, you will be pro True True False - False True True add_identifier_radiobutton @@ -3567,7 +3054,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False 0 12 When a photo or video of the same name has already been downloaded, choose whether to skip downloading the file, or to add a unique indentifier. @@ -3584,7 +3070,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False 0 12 When backing up, choose whether to overwrite a file on the backup device that has the same name, or skip backing it up. @@ -3604,10 +3089,9 @@ When this option is enabled, and a potential device is detected, you will be pro True True False - False True backup_duplicate_skip_radiobutton - + 1 @@ -3622,10 +3106,9 @@ When this option is enabled, and a potential device is detected, you will be pro True True False - False True True - + 1 @@ -3693,26 +3176,21 @@ When this option is enabled, and a potential device is detected, you will be pro - True - True 1 True - False False - True 2 False - True 12 1 @@ -3725,7 +3203,6 @@ When this option is enabled, and a potential device is detected, you will be pro True - False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Error Handling @@ -3736,16 +3213,12 @@ When this option is enabled, and a potential device is detected, you will be pro - True - True 6 1 - True - True 5 1 @@ -3753,6 +3226,46 @@ When this option is enabled, and a potential device is detected, you will be pro + + + True + end + + + gtk-help + True + True + True + True + + + False + False + 0 + True + + + + + gtk-close + True + True + True + True + + + False + False + 1 + + + + + False + end + 0 + + diff --git a/rapid/glade3/rapid.ui b/rapid/glade3/rapid.ui index 1e49e4e..3ec67f4 100644 --- a/rapid/glade3/rapid.ui +++ b/rapid/glade3/rapid.ui @@ -1,73 +1,66 @@ - - About... + About... gtk-about - Check All + Check All - Check All Photos + Check All Photos - Check All Videos + Check All Videos - Download + Download system-run False - Get Help Online... + Get Help Online... gtk-help - Help + Help - Next File Next file - gtk-go-forward - True - Preferences + Preferences - Previous File Previous file - gtk-go-back - True - Quit + Quit gtk-quit - Refresh + Refresh gtk-refresh - Report a Problem... + Report a Problem... gtk-dialog-warning @@ -80,11 +73,11 @@ - Translate this Application... + Translate this Application... - Uncheck All + Uncheck All @@ -102,7 +95,6 @@ True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True True @@ -113,7 +105,6 @@ False - True Download / Pause @@ -151,7 +142,6 @@ True False quit_action - False True True @@ -171,7 +161,6 @@ False - True True @@ -244,7 +233,6 @@ False - True gtk-zoom-in @@ -253,8 +241,8 @@ False True True - + @@ -304,6 +292,8 @@ True False prev_image_action + False + Previous File @@ -312,6 +302,8 @@ True False next_image_action + False + Next File @@ -329,7 +321,6 @@ False - True True @@ -352,7 +343,6 @@ True False donate_action - False _Make a Donation... True @@ -362,7 +352,6 @@ True False translate_action - False _Translate this Application... True @@ -379,7 +368,6 @@ True False about_action - False True True @@ -395,6 +383,69 @@ 0 + + + True + False + True + + + True + False + vertical + False + + + True + True + 0 + + + + + True + False + vertical + False + + + True + True + 1 + + + + + True + False + vertical + False + + + True + True + 2 + + + + + False + vertical + + + False + False + end + 3 + + + + + False + True + 1 + + True @@ -402,21 +453,33 @@ 1 True - + True - True - never - automatic + False - + True False - queue - none + never + etched-out - + + True + False + queue + none + + + + + + True + True + 6 + 0 + @@ -442,8 +505,6 @@ True True - automatic - automatic @@ -581,7 +642,6 @@ True True next_image_action - False none False @@ -602,7 +662,6 @@ True True prev_image_action - False none False @@ -740,7 +799,7 @@ True True - 1 + 2 @@ -751,7 +810,6 @@ True False - edge _Help @@ -797,7 +855,7 @@ False True 6 - 2 + 3 @@ -832,7 +890,6 @@ True False 2 - False True @@ -984,7 +1041,7 @@ False False end - 3 + 4 diff --git a/rapid/metadataexiftool.py b/rapid/metadataexiftool.py new file mode 100755 index 0000000..d6b61fc --- /dev/null +++ b/rapid/metadataexiftool.py @@ -0,0 +1,225 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch + +### This program 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 2 of the License, or +### (at your option) any later version. + +### This program 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 this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import subprocess +import json +import datetime, time +import string + +import multiprocessing +import logging +logger = multiprocessing.get_logger() + +def version_info(): + """returns the version of exiftool being used""" + try: + # unfortunately subprocess.check_output does not exist on python 2.6 + proc = subprocess.Popen(['exiftool', '-ver'], stdout=subprocess.PIPE) + v = proc.communicate()[0].strip() + except OSError: + v = None + return v + +EXIFTOOL_VERSION = version_info() + +class ExifToolMetaData: + """ + Class to use when a python based metadata parser fails to correctly load + necessary metadata. Calls exiftool as a subprocess. It is therefore slow, + but in contrast to exiv2 or kaa metadata, exiftool somtimes gives better + output. + """ + def __init__(self, filename): + self.filename = filename + self.metadata = None + self.metadata_string_format = None + self.exiftool_error = "Error encountered using exiftool with file %s" + self.exiftool_output = "Unexpected output from exiftool with file %s" + + def _get(self, key, missing): + + if key == "VideoStreamType" or "FileNumber": + # special case: want exiftool's string formatting + if self.metadata_string_format is None: + try: + proc = subprocess.Popen(['exiftool', '-j', self.filename], stdout=subprocess.PIPE) + s = proc.communicate()[0] + except: + logger.error(self.exiftool_error, self.filename) + return missing + try: + self.metadata_string_format = json.loads(s) + except: + logger.error(self.exiftool_output, self.filename) + return missing + + try: + v = self.metadata_string_format[0][key] + except: + return missing + return v + + elif self.metadata is None: + # note: exiftool's string formatting is OFF (-n switch) + try: + proc = subprocess.Popen(['exiftool', '-j', '-n', self.filename], stdout=subprocess.PIPE) + s = proc.communicate()[0] + except: + logger.error(self.exiftool_error, self.filename) + return missing + try: + self.metadata = json.loads(s) + except: + logger.error(self.exiftool_output, self.filename) + return missing + + try: + v = self.metadata[0][key] + except: + return missing + return v + + + def date_time(self, missing=''): + """ + Returns in python datetime format the date and time the image was + recorded. + + Trys to get value from key "DateTimeOriginal" + If that fails, tries "CreateDate" + + Returns missing either metadata value is not present. + """ + d = self._get('DateTimeOriginal', None) + if d is None: + d = self._get('CreateDate', None) + if d is None: + d = self._get('FileModifyDate', None) + if d is not None: + try: + # returned value may or may not have a time offset + # strip it if need be + dt = d[:19] + dt = datetime.datetime.strptime(dt, "%Y:%m:%d %H:%M:%S") + except: + logger.error("Error reading date metadata with file %s", self.filename) + return missing + + return dt + else: + return missing + + def time_stamp(self, missing=''): + """ + Returns a float value representing the time stamp, if it exists + """ + dt = self.date_time(missing=None) + if dt: + # convert it to a time stamp (not optimal, but better than nothing!) + v = time.mktime(dt.timetuple()) + else: + v = missing + return v + + def file_number(self, missing=''): + v = self._get("FileNumber", None) + if v is not None: + return str(v) + else: + return missing + + def width(self, missing=''): + v = self._get('ImageWidth', None) + if v is not None: + return str(v) + else: + return missing + + def height(self, missing=''): + v = self._get('ImageHeight', None) + if v is not None: + return str(v) + else: + return missing + + def length(self, missing=''): + """ + return the duration (length) of the video, rounded to the nearest second, in string format + """ + v = self._get("Duration", None) + if v is not None: + try: + v = float(v) + v = "%0.f" % v + except: + return missing + return v + else: + return missing + + def frames_per_second(self, stream=0, missing=''): + """ + value stream is ignored (kept for compatibilty with code calling kaa) + """ + v = self._get("FrameRate", None) + if v is None: + v = self._get("VideoFrameRate", None) + + if v is None: + return missing + try: + v = '%.0f' % v + except: + return missing + return v + + def codec(self, stream=0, missing=''): + """ + value stream is ignored (kept for compatibilty with code calling kaa) + """ + v = self._get("VideoStreamType", None) + if v is None: + v = self._get("VideoCodec", None) + if v is not None: + return v + else: + return missing + + def fourcc(self, stream=0, missing=''): + """ + value stream is ignored (kept for compatibilty with code calling kaa) + """ + return self._get("CompressorID", missing) + +if __name__ == '__main__': + import sys + + + if (len(sys.argv) != 2): + print 'Usage: ' + sys.argv[0] + ' path/to/video/containing/metadata' + sys.exit(0) + + else: + m = ExifToolMetaData(sys.argv[1]) + dt = m.date_time() + print dt + print "%sx%s" % (m.width(), m.height()) + print m.length() + print m.frames_per_second() + print m.codec() diff --git a/rapid/metadataphoto.py b/rapid/metadataphoto.py old mode 100755 new mode 100644 index 0442a86..1b36be9 --- a/rapid/metadataphoto.py +++ b/rapid/metadataphoto.py @@ -30,7 +30,7 @@ except ImportError: sys.stderr.write("You need to install pyexiv2, the python binding for exiv2, to run this program.\n" ) sys.exit(1) - +import metadataexiftool def __version_info(version): if not version: @@ -39,7 +39,7 @@ def __version_info(version): v = '' for i in version: v += '.%s' % i - return v[1:] + return v[1:] def pyexiv2_version_info(): return __version_info(pyexiv2.version_info) @@ -54,6 +54,16 @@ class MetaData(pyexiv2.metadata.ImageMetadata): """ + def __init__(self, full_file_name): + pyexiv2.metadata.ImageMetadata.__init__(self, full_file_name) + self.rpd_metadata_exiftool = None + self.rpd_full_file_name = full_file_name + + def _load_exiftool(self): + if self.rpd_metadata_exiftool is None: + self.rpd_metadata_exiftool = metadataexiftool.ExifToolMetaData(self.rpd_full_file_name) + + def aperture(self, missing=''): """ Returns in string format the floating point value of the image's aperture. @@ -211,6 +221,19 @@ class MetaData(pyexiv2.metadata.ImageMetadata): except: return missing + def file_number(self, missing=''): + """returns Exif.CanonFi.FileNumber, not to be confused with + Exif.Canon.FileNumber""" + try: + if 'Exif.CanonFi.FileNumber' in self.exif_keys: + self._load_exiftool() + return self.rpd_metadata_exiftool.file_number(missing) + else: + return missing + except: + return missing + + def owner_name(self, missing=''): """ returns camera name recorded by select Canon cameras""" try: diff --git a/rapid/metadatavideo.py b/rapid/metadatavideo.py index 7b6bc6c..5519143 100644 --- a/rapid/metadatavideo.py +++ b/rapid/metadatavideo.py @@ -17,13 +17,13 @@ ### along with this program; if not, write to the Free Software ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +HAVE_HACHOIR = True DOWNLOAD_VIDEO = True import os import datetime import time import subprocess -import tempfile import multiprocessing import logging @@ -33,15 +33,30 @@ import gtk import paths import rpdfile +import metadataexiftool + +from gettext import gettext as _ try: from hachoir_core.cmd_line import unicodeFilename from hachoir_parser import createParser from hachoir_metadata import extractMetadata except ImportError: - DOWNLOAD_VIDEO = False + HAVE_HACHOIR = False + +if not HAVE_HACHOIR: + v = metadataexiftool.version_info() + if v is None: + DOWNLOAD_VIDEO = False + +def file_types_to_download(): + """Returns a string with the types of file to download, to display to the user""" + if DOWNLOAD_VIDEO: + return _("photos and videos") + else: + return _("photos") -if DOWNLOAD_VIDEO: +if HAVE_HACHOIR: def version_info(): from hachoir_metadata.version import VERSION diff --git a/rapid/metadataxmp.py b/rapid/metadataxmp.py new file mode 100755 index 0000000..47321e3 --- /dev/null +++ b/rapid/metadataxmp.py @@ -0,0 +1,198 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011-12 Damon Lynch + +### This program 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 2 of the License, or +### (at your option) any later version. + +### This program 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 this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import subprocess + +import multiprocessing, logging +logger = multiprocessing.get_logger() + +class XmpMetadataSidecar: + + def __init__(self, filename): + self.filename = filename + self.keys = [] + + def _add_pair(self, key_value_pair): + self.keys.append(key_value_pair) + logger.debug(key_value_pair) + + def _generate_exiv2_command_line(self): + # -f option: overwrites any existing xmp file + return ['exiv2', '-f'] + self.keys + ['-exX', self.filename] + + def _generate_exiv2_contact_info(self, key, value): + return "-M set Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:%s %s" % (key, value) + + def _generate_exiv2_dc(self, key, value): + return "-M set Xmp.dc.%s %s" % (key, value) + + def _generate_exiv2_photoshop(self, key, value): + return "-M set Xmp.photoshop.%s %s" % (key, value) + + def _generate_exiv2_rights(self, key, value): + return "-M set Xmp.xmpRights.%s %s" % (key, value) + + def _generate_exiv2_iptc(self, key, value): + return "-M set Xmp.iptc.%s %s" % (key, value) + + def _generate_exiv2_exif(self, key, value): + return "-M set Xmp.exif.%s %s" % (key, value) + + def set_location(self, location): + self._add_pair(self._generate_exiv2_iptc('Location', location)) + + def set_city(self, city): + self._add_pair(self._generate_exiv2_photoshop('City', city)) + + def set_state_province(self, state): + self._add_pair(self._generate_exiv2_photoshop('State', state)) + + def set_country(self, country): + self._add_pair(self._generate_exiv2_photoshop('Country', country)) + + def set_country_code(self, country_code): + self._add_pair(self._generate_exiv2_iptc('CountryCode', country_code)) + + def set_headline(self, headline): + self._add_pair(self._generate_exiv2_photoshop('Headline', headline)) + + def set_description_writer(self, writer): + """ + Synonym: Caption writer + """ + self._add_pair(self._generate_exiv2_photoshop('CaptionWriter', writer)) + + def set_description(self, description): + """A synonym for this in some older programs is 'Caption'""" + self._add_pair(self._generate_exiv2_dc('description', description)) + + def set_subject(self, subject): + """ + You can call this more than once, to add multiple subjects + + A synonym is 'Keywords' + """ + self._add_pair(self._generate_exiv2_dc('subject', subject)) + + def set_creator(self, creator): + """ + Sets the author (creator) field. Photo Mechanic calls this 'Photographer'. + """ + self._add_pair(self._generate_exiv2_dc('creator', creator)) + + def set_creator_job_title(self, job_title): + self._add_pair(self._generate_exiv2_photoshop('AuthorsPosition', job_title)) + + def set_credit_line(self, credit_line): + self._add_pair(self._generate_exiv2_photoshop('Credit', credit_line)) + + def set_source(self, source): + """ + original owner or copyright holder of the photograph + """ + self._add_pair(self._generate_exiv2_photoshop('Source', source)) + + def set_copyright(self, copyright): + self._add_pair(self._generate_exiv2_dc('rights', copyright)) + + def set_copyright_url(self, copyright_url): + self._add_pair(self._generate_exiv2_rights('WebStatement', copyright_url)) + + def set_contact_city(self, city): + self._add_pair(self._generate_exiv2_contact_info('CiAdrCity', city)) + + def set_contact_country(self, country): + self._add_pair(self._generate_exiv2_contact_info('CiAdrCtry', country)) + + def set_contact_address(self, address): + """The contact information address part. + Comprises an optional company name and all required information + to locate the building or postbox to which mail should be sent.""" + self._add_pair(self._generate_exiv2_contact_info('CiAdrExtadr', address)) + + def set_contact_postal_code(self, postal_code): + self._add_pair(self._generate_exiv2_contact_info('CiAdrPcode', postal_code)) + + def set_contact_region(self, region): + """State/Province""" + self._add_pair(self._generate_exiv2_contact_info('CiAdrRegion', region)) + + def set_contact_email(self, email): + """Multiple email addresses can be given, separated by a comma.""" + self._add_pair(self._generate_exiv2_contact_info('CiEmailWork', email)) + + def set_contact_telephone(self, telephone): + """Multiple numbers can be given, separated by a comma.""" + self._add_pair(self._generate_exiv2_contact_info('CiTelWork', telephone)) + + def set_contact_url(self, url): + """Multiple URLs can be given, separated by a comma.""" + self._add_pair(self._generate_exiv2_contact_info('CiUrlWork', url)) + + def set_exif_value(self, key, value): + self._add_pair(self._generate_exiv2_exif(key, value)) + + def write_xmp_sidecar(self): + cmd = self._generate_exiv2_command_line() + if logger.getEffectiveLevel() == logging.DEBUG: + cmd_line = '' + for c in cmd: + cmd_line += c + ' ' + cmd_line = cmd_line.strip() + logger.debug("XMP write command: %s", cmd_line) + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) + return proc.communicate()[0].strip() + + +if __name__ == '__main__': + import sys + + + if (len(sys.argv) != 2): + print 'Usage: ' + sys.argv[0] + ' path/to/photo/containing/metadata' + + else: + x = XmpMetadataSidecar(sys.argv[1]) + x.set_description("This is image is just a sample and is nothing serious. I used to test out writing XMP files in Rapid Photo Downloader.") + x.set_description_writer("Damon Lynch wrote caption") + x.set_headline("Sample image to test XMP") + x.set_subject("Keyword 1") + x.set_subject("Keyword 2") + x.set_city("Minneapolis") + x.set_location("University of Minnesota") + x.set_state_province("Minnesota") + x.set_country("United States of America") + x.set_country_code("USA") + x.set_creator("Damon Lynch") + x.set_creator_job_title("Photographer") + x.set_credit_line("Contact Damon for permission") + x.set_source("Damon Lynch is the original photographer") + x.set_copyright("© 2011 Damon Lynch, all rights reserved.") + x.set_copyright_url("http://www.damonlynch.net/license") + x.set_contact_address("Contact house number, street, apartment") + x.set_contact_city('Contact City') + x.set_contact_region('Contact State') + x.set_contact_postal_code('Contact Post code') + x.set_contact_telephone('+1 111 111 1111') + x.set_contact_country('Contact Country') + x.set_contact_address('Address\nApartment') + x.set_contact_url('http://www.sample.net') + x.set_contact_email('name@email1.com, name@email2.com') + + x.write_xmp_sidecar() diff --git a/rapid/misc.py b/rapid/misc.py index 1e023ad..e748dc3 100644 --- a/rapid/misc.py +++ b/rapid/misc.py @@ -35,7 +35,7 @@ def run_dialog( text, secondarytext=None, parent=None, messagetype=gtk.MESSAGE_ text ) if parent: - d.set_transient_for(parent.widget.get_toplevel()) + d.set_transient_for(parent.get_toplevel()) for b,rid in extrabuttons: d.add_button(b,rid) d.vbox.set_spacing(12) diff --git a/rapid/preferencesdialog.py b/rapid/preferencesdialog.py index 1509630..bc9e0b7 100644 --- a/rapid/preferencesdialog.py +++ b/rapid/preferencesdialog.py @@ -36,6 +36,7 @@ import rpdfile import higdefaults as hd import metadataphoto import metadatavideo + import tableplusminus as tpm import utilities @@ -385,7 +386,8 @@ class VideoSubfolderPrefs(PhotoSubfolderPrefs): pref_list = pref_list) class QuestionDialog(gtk.Dialog): - def __init__(self, parent_window, title, question, post_choice_callback): + def __init__(self, parent_window, title, question, use_markup=False, + default_to_yes=True, post_choice_callback=None): gtk.Dialog.__init__(self, title, None, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, (gtk.STOCK_NO, gtk.RESPONSE_CANCEL, @@ -404,6 +406,7 @@ class QuestionDialog(gtk.Dialog): prompt_hbox.pack_start(image, False, False, padding = 6) prompt_label = gtk.Label(question) + prompt_label.set_use_markup(use_markup) prompt_label.set_line_wrap(True) prompt_hbox.pack_start(prompt_label, False, False, padding=6) @@ -412,16 +415,18 @@ class QuestionDialog(gtk.Dialog): self.set_border_width(6) self.set_has_separator(False) - self.set_default_response(gtk.RESPONSE_OK) + if default_to_yes: + self.set_default_response(gtk.RESPONSE_OK) + else: + self.set_default_response(gtk.RESPONSE_CANCEL) - self.set_transient_for(parent_window) self.show_all() - - self.connect('response', self.on_response) + if post_choice_callback: + self.connect('response', self.on_response) - def on_response(self, device_dialog, response): + def on_response(self, device_dialog, response): user_selected = response == gtk.RESPONSE_OK self.post_choice_callback(self, user_selected) @@ -430,21 +435,21 @@ class RemoveAllJobCodeDialog(QuestionDialog): QuestionDialog.__init__(self, parent_window, _('Remove all Job Codes?'), _('Should all Job Codes be removed?'), - post_choice_callback) + post_choice_callback=post_choice_callback) class RemoveAllRemeberedDevicesDialog(QuestionDialog): def __init__(self, parent_window, post_choice_callback): QuestionDialog.__init__(self, parent_window, _('Remove all Remembered Paths?'), _('Should all remembered paths be removed?'), - post_choice_callback) + post_choice_callback=post_choice_callback) class RemoveAllIgnoredPathsDialog(QuestionDialog): def __init__(self, parent_window, post_choice_callback): QuestionDialog.__init__(self, parent_window, _('Remove all Ignored Paths?'), _('Should all ignored paths be removed?'), - post_choice_callback) + post_choice_callback=post_choice_callback) class PhotoRenameTable(tpm.TablePlusMinus): @@ -467,8 +472,7 @@ class PhotoRenameTable(tpm.TablePlusMinus): self.connect("add", self.size_adjustment) self.connect("remove", self.size_adjustment) - # get scrollbar thickness from parent app scrollbar - very hackish, but what to do?? - self.bump = 16# self.preferencesdialog.parentApp.image_scrolledwindow.get_hscrollbar().allocation.height + self.bump = 16 self.have_vertical_scrollbar = False @@ -887,10 +891,7 @@ class PreferencesDialog(): self._setup_control_spacing() - if metadatavideo.DOWNLOAD_VIDEO: - self.file_types = _("photos and videos") - else: - self.file_types = _("photos") + self.file_types = metadatavideo.file_types_to_download() self._setup_sample_names() @@ -1418,10 +1419,10 @@ class PreferencesDialog(): self.prefs.auto_exit) self.auto_exit_force_checkbutton.set_active( self.prefs.auto_exit_force) - self.auto_delete_checkbutton.set_active( - self.prefs.auto_delete) self.generate_thumbnails_checkbutton.set_active( self.prefs.generate_thumbnails) + self.auto_rotate_checkbutton.set_active( + self.prefs.auto_rotate_jpeg) self.update_misc_controls() @@ -1758,9 +1759,9 @@ class PreferencesDialog(): def on_auto_unmount_checkbutton_toggled(self, checkbutton): self.prefs.auto_unmount = checkbutton.get_active() - - def on_auto_delete_checkbutton_toggled(self, checkbutton): - self.prefs.auto_delete = checkbutton.get_active() + + def on_auto_rotate_checkbutton_toggled(self, checkbutton): + self.prefs.auto_rotate_jpeg = checkbutton.get_active() def on_auto_exit_checkbutton_toggled(self, checkbutton): active = checkbutton.get_active() @@ -1918,7 +1919,6 @@ class PreferencesDialog(): def on_backup_identifier_entry_changed(self, widget): self.update_backup_example() - #~ self.prefs. def on_video_backup_identifier_entry_changed(self, widget): self.update_backup_example() diff --git a/rapid/prefsrapid.py b/rapid/prefsrapid.py index 080e6f9..efd06b5 100644 --- a/rapid/prefsrapid.py +++ b/rapid/prefsrapid.py @@ -136,6 +136,7 @@ class RapidPreferences(prefs.Preferences): "show_warning_downloading_from_camera": prefs.Value(prefs.BOOL, True), #~ "preview_zoom": prefs.Value(prefs.INT, zoom), "generate_thumbnails": prefs.Value(prefs.BOOL, True), + "auto_rotate_jpeg": prefs.Value(prefs.BOOL, True), } def __init__(self): diff --git a/rapid/rapid.py b/rapid/rapid.py index db5bdc6..9c423c2 100755 --- a/rapid/rapid.py +++ b/rapid/rapid.py @@ -60,9 +60,12 @@ import generatename as gn import downloadtracker -from metadatavideo import DOWNLOAD_VIDEO +import filemodify + +from metadatavideo import DOWNLOAD_VIDEO, file_types_to_download import metadataphoto import metadatavideo +import metadataexiftool import scan as scan_process import copyfiles @@ -143,12 +146,14 @@ class DeviceCollection(gtk.TreeView): # make it impossible to select a row selection = self.get_selection() selection.set_mode(gtk.SELECTION_NONE) + self.set_headers_visible(False) # Device refers to a thing like a camera, memory card in its reader, # external hard drive, Portable Storage Device, etc. column0 = gtk.TreeViewColumn(_("Device")) pixbuf_renderer = gtk.CellRendererPixbuf() + pixbuf_renderer.set_padding(2, 0) text_renderer = gtk.CellRendererText() text_renderer.props.ellipsize = pango.ELLIPSIZE_MIDDLE text_renderer.set_fixed_size(160, -1) @@ -216,7 +221,7 @@ class DeviceCollection(gtk.TreeView): # (with one card at startup), it could be 21 # must account for header row at the top row_height = self.get_background_area(0, self.get_column(0))[3] + 1 - height = (len(self.map_process_to_row) + 1) * row_height + height = max(((len(self.map_process_to_row) + 1) * row_height), 24) self.parent_app.device_collection_scrolledwindow.set_size_request(-1, height) def update_device(self, process_id, total_size_files): @@ -683,9 +688,13 @@ class ThumbnailDisplay(gtk.IconView): #check if preview should be from a downloaded file, or the source if rpd_file.status in DOWNLOADED: file_location = rpd_file.download_full_file_name + thm_file_name = rpd_file.download_thm_full_name else: file_location = rpd_file.full_file_name + thm_file_name = rpd_file.thm_full_name + self.preview_manager.get_preview(unique_id, file_location, + thm_file_name, rpd_file.file_type, size_max=None,) self.previews_being_fetched.add(unique_id) @@ -1188,10 +1197,14 @@ class CopyFilesManager(TaskManager): video_download_folder = task[1] scan_pid = task[2] files = task[3] + modify_files_during_download = task[4] + modify_pipe = task[5] copy_files = copyfiles.CopyFiles(photo_download_folder, video_download_folder, files, + modify_files_during_download, + modify_pipe, scan_pid, self.batch_size, task_process_conn, terminate_queue, run_event) copy_files.start() @@ -1209,12 +1222,51 @@ class ThumbnailManager(TaskManager): generator.start() self._processes.append((generator, terminate_queue, run_event)) return generator.pid + +class FileModifyManager(TaskManager): + """Handles the modification of downloaded files before they are renamed + Duplex, multiprocess, similar to BackupFilesManager + """ + def __init__(self, results_callback): + TaskManager.__init__(self, results_callback=results_callback, + batch_size=0) + self.file_modify_by_scan_pid = {} + + def _initiate_task(self, task, task_results_conn, task_process_conn, + terminate_queue, run_event): + scan_pid = task[0] + auto_rotate_jpeg = task[1] + focal_length = task[2] + + file_modify = filemodify.FileModify(auto_rotate_jpeg, focal_length, + task_process_conn, terminate_queue, + run_event) + file_modify.start() + self._processes.append((file_modify, terminate_queue, run_event, + task_results_conn)) + + self.file_modify_by_scan_pid[scan_pid] = (task_results_conn, file_modify.pid) + + return file_modify.pid + + def _setup_pipe(self): + return Pipe(duplex=True) + + def _send_termination_msg(self, p): + p[1].put(None) + p[3].send((None, None)) + + def get_modify_pipe(self, scan_pid): + return self.file_modify_by_scan_pid[scan_pid][0] + class BackupFilesManager(TaskManager): """ - Handles backup processes. This is a little different from other Task + Handles backup processes. This is a little different from some other Task Manager classes in that its pipe is Duplex, and the work done by it is not pre-assigned when the process is started. + + Duplex, multiprocess. """ def __init__(self, results_callback, batch_size): TaskManager.__init__(self, results_callback, batch_size) @@ -1300,8 +1352,8 @@ class PreviewManager(SingleInstanceTaskManager): self._get_preview = tn.GetPreviewImage(self.task_process_conn) self._get_preview.start() - def get_preview(self, unique_id, full_file_name, file_type, size_max): - self.task_results_conn.send((unique_id, full_file_name, file_type, size_max)) + def get_preview(self, unique_id, full_file_name, thm_file_name, file_type, size_max): + self.task_results_conn.send((unique_id, full_file_name, thm_file_name, file_type, size_max)) def task_results(self, source, condition): unique_id, preview_full_size, preview_small = self.task_results_conn.recv() @@ -1312,10 +1364,10 @@ class SubfolderFileManager(SingleInstanceTaskManager): """ Manages the daemon process that renames files and creates subfolders """ - def __init__(self, results_callback, sequence_values, focal_length): + def __init__(self, results_callback, sequence_values): SingleInstanceTaskManager.__init__(self, results_callback) self._subfolder_file = subfolderfile.SubfolderFile(self.task_process_conn, - sequence_values, focal_length) + sequence_values) self._subfolder_file.start() logger.debug("SubfolderFile PID: %s", self._subfolder_file.pid) @@ -1440,7 +1492,6 @@ class PreviewImage: def update_preview_image(self, unique_id, pil_image): if unique_id == self.unique_id: self.set_preview_image(unique_id, pil_image) - class RapidApp(dbus.service.Object): @@ -1525,6 +1576,7 @@ class RapidApp(dbus.service.Object): scan_termination_requested = self.scan_manager.request_termination() thumbnails_termination_requested = self.thumbnails.thumbnail_manager.request_termination() backup_termination_requested = self.backup_manager.request_termination() + file_modify_termination_requested = self.file_modify_manager.request_termination() if terminate_file_copies: copy_files_termination_requested = self.copy_files_manager.request_termination() @@ -1532,17 +1584,19 @@ class RapidApp(dbus.service.Object): copy_files_termination_requested = False if (scan_termination_requested or thumbnails_termination_requested or - backup_termination_requested): + backup_termination_requested or file_modify_termination_requested): time.sleep(1) if (self.scan_manager.get_no_active_processes() > 0 or self.thumbnails.thumbnail_manager.get_no_active_processes() > 0 or - self.backup_manager.get_no_active_processes() > 0): + self.backup_manager.get_no_active_processes() > 0 or + self.file_modify_manager.get_no_active_processes() > 0): time.sleep(1) # must try again, just in case a new scan has meanwhile started! self.scan_manager.request_termination() self.thumbnails.thumbnail_manager.terminate_forcefully() self.scan_manager.terminate_forcefully() self.backup_manager.terminate_forcefully() + self.file_modify_manager.terminate_forcefully() if terminate_file_copies and copy_files_termination_requested: time.sleep(1) @@ -1591,6 +1645,7 @@ class RapidApp(dbus.service.Object): self.on_rapidapp_destroy(widget=self.rapidapp, data=None) def on_refresh_action_activate(self, action): + self.thumbnails.clear_all() self.setup_devices(on_startup=False, on_preference_change=False, block_auto_start=True) @@ -1602,7 +1657,7 @@ class RapidApp(dbus.service.Object): self.about.set_property("version", utilities.human_readable_version( __version__)) self.about.run() - self.about.destroy() + self.about.hide() def on_report_problem_action_activate(self, action): webbrowser.open("https://bugs.launchpad.net/rapid") @@ -1675,6 +1730,34 @@ class RapidApp(dbus.service.Object): self.prefs.ignored_paths, self.prefs.use_re_ignored_paths]) + def confirm_manual_location(self): + """ + Queries the user to ask if they really want to download from locations + that could take a very long time to scan. They can choose yes or no. + + Returns True if yes or there was no need to ask the user, False if the + user said no. + """ + l = self.prefs.device_location + if l in ['/media', os.path.expanduser('~'), '/']: + logger.info("Prompting whether to download from %s", l) + if l == '/': + #this location is a human readable explanation for /, and is inserted into Downloading from %(location)s + l = _('the root of the file system') + c = preferencesdialog.QuestionDialog(parent_window=self.rapidapp, + title=_('Rapid Photo Downloader'), + #message in dialog box which asks the user if they really want to be downloading from this location + question="" + _("Downloading from %(location)s.") % {'location': l} + "\n\n" + + _("Do you really want to download from here? On some systems, scanning this location can take a very long time."), + default_to_yes=False, + use_markup=True) + response = c.run() + user_confirmed = response == gtk.RESPONSE_OK + c.destroy() + if not user_confirmed: + return False + return True + def setup_devices(self, on_startup, on_preference_change, block_auto_start): """ @@ -1697,10 +1780,12 @@ class RapidApp(dbus.service.Object): if self.using_volume_monitor(): self.start_volume_monitor() - self.clear_non_running_downloads() - + if not self.prefs.device_autodetection: + if not self.confirm_manual_location(): + return + mounts = [] self.backup_devices = {} @@ -2175,6 +2260,10 @@ class RapidApp(dbus.service.Object): # Track which downloads are running self.download_active_by_scan_pid = [] + + def modify_files_during_download(self): + """ Returns True if there is a need to modify files during download""" + return self.prefs.auto_rotate_jpeg or (self.focal_length is not None) def start_download(self, scan_pid=None): @@ -2286,11 +2375,20 @@ class RapidApp(dbus.service.Object): if self.auto_start_is_on and self.prefs.generate_thumbnails: for rpd_file in files: rpd_file.generate_thumbnail = True + + modify_files_during_download = self.modify_files_during_download() + if modify_files_during_download: + self.file_modify_manager.add_task((scan_pid, self.prefs.auto_rotate_jpeg, self.focal_length)) + modify_pipe = self.file_modify_manager.get_modify_pipe(scan_pid) + else: + modify_pipe = None + # Initiate copy files process self.copy_files_manager.add_task((photo_download_folder, video_download_folder, scan_pid, - files)) + files, modify_files_during_download, + modify_pipe)) def copy_files_results(self, source, condition): """ @@ -2315,33 +2413,7 @@ class RapidApp(dbus.service.Object): None, None) self.time_remaining.update(scan_pid, bytes_downloaded=chunk_downloaded) elif msg_type == rpdmp.MSG_FILE: - download_succeeded, rpd_file, download_count, temp_full_file_name, thumbnail_icon, thumbnail = data - - if thumbnail is not None or thumbnail_icon is not None: - self.thumbnails.update_thumbnail((rpd_file.unique_id, - thumbnail_icon, - thumbnail)) - - self.download_tracker.set_download_count_for_file( - rpd_file.unique_id, download_count) - self.download_tracker.set_download_count( - rpd_file.scan_pid, download_count) - rpd_file.download_start_time = self.download_start_time - - if download_succeeded: - # Insert preference values needed for name generation - rpd_file = prefsrapid.insert_pref_lists(self.prefs, rpd_file) - rpd_file.strip_characters = self.prefs.strip_characters - rpd_file.download_folder = self.prefs.get_download_folder_for_file_type(rpd_file.file_type) - rpd_file.download_conflict_resolution = self.prefs.download_conflict_resolution - rpd_file.synchronize_raw_jpg = self.prefs.must_synchronize_raw_jpg() - rpd_file.job_code = self.job_code - - self.subfolder_file_manager.rename_file_and_move_to_subfolder( - download_succeeded, - download_count, - rpd_file - ) + self.copy_file_results_single_file(data) return True else: @@ -2350,6 +2422,63 @@ class RapidApp(dbus.service.Object): return False + def copy_file_results_single_file(self, data): + """ + Handles results from one of two processes: + 1. copy_files + 2. file_modify + + Operates after a single file has been copied from the download device + to the local folder. + + Calls the process to rename files and create subfolders (subfolderfile) + """ + + download_succeeded, rpd_file, download_count, temp_full_file_name, thumbnail_icon, thumbnail = data + + if thumbnail is not None or thumbnail_icon is not None: + self.thumbnails.update_thumbnail((rpd_file.unique_id, + thumbnail_icon, + thumbnail)) + + self.download_tracker.set_download_count_for_file( + rpd_file.unique_id, download_count) + self.download_tracker.set_download_count( + rpd_file.scan_pid, download_count) + rpd_file.download_start_time = self.download_start_time + + if download_succeeded: + # Insert preference values needed for name generation + rpd_file = prefsrapid.insert_pref_lists(self.prefs, rpd_file) + rpd_file.strip_characters = self.prefs.strip_characters + rpd_file.download_folder = self.prefs.get_download_folder_for_file_type(rpd_file.file_type) + rpd_file.download_conflict_resolution = self.prefs.download_conflict_resolution + rpd_file.synchronize_raw_jpg = self.prefs.must_synchronize_raw_jpg() + rpd_file.job_code = self.job_code + + self.subfolder_file_manager.rename_file_and_move_to_subfolder( + download_succeeded, + download_count, + rpd_file + ) + def file_modify_results(self, source, condition): + """ + 'file modify' is a process that runs immediately after 'copy files', + meaning there can be more than one at one time. + + It runs before the renaming process. + """ + connection = self.file_modify_manager.get_pipe(source) + + conn_type, data = connection.recv() + if conn_type == rpdmp.CONN_PARTIAL: + self.copy_file_results_single_file(data) + return True + else: + # Process is complete, i.e. conn_type == rpdmp.CONN_COMPLETE + connection.close() + return False + def download_is_occurring(self): """Returns True if a file is currently being downloaded, renamed or @@ -2840,8 +2969,10 @@ class RapidApp(dbus.service.Object): 'device_location', 'ignored_paths', 'use_re_ignored_paths', 'device_blacklist']: self.rerun_setup_available_image_and_video_media = True + self._set_from_toolbar_state() if not self.preferences_dialog_displayed: self.post_preference_change() + elif key in ['backup_images', 'backup_device_autodetection', 'backup_location', 'backup_video_location', @@ -2868,6 +2999,7 @@ class RapidApp(dbus.service.Object): self._check_for_sequence_value_use() elif key in ['download_folder', 'video_download_folder']: + self._set_to_toolbar_values() self.display_free_space() def post_preference_change(self): @@ -2945,9 +3077,14 @@ class RapidApp(dbus.service.Object): self.builder = builder builder.add_from_file(paths.share_dir("glade3/rapid.ui")) self.rapidapp = builder.get_object("rapidapp") + self.from_toolbar = builder.get_object("from_toolbar") + self.copy_toolbar = builder.get_object("copy_toolbar") + self.dest_toolbar = builder.get_object("dest_toolbar") + self.menu_toolbar = builder.get_object("menu_toolbar") self.main_vpaned = builder.get_object("main_vpaned") self.main_notebook = builder.get_object("main_notebook") self.download_action = builder.get_object("download_action") + self.download_button = builder.get_object("download_button") self.download_progressbar = builder.get_object("download_progressbar") self.rapid_statusbar = builder.get_object("rapid_statusbar") @@ -2962,7 +3099,9 @@ class RapidApp(dbus.service.Object): # Only enable this action when actually displaying a preview self.next_image_action.set_sensitive(False) - self.prev_image_action.set_sensitive(False) + self.prev_image_action.set_sensitive(False) + + self._init_toolbars() # About dialog builder.add_from_file(paths.share_dir("glade3/about.ui")) @@ -3003,6 +3142,227 @@ class RapidApp(dbus.service.Object): self.time_remaining = downloadtracker.TimeRemaining() self.time_check = downloadtracker.TimeCheck() + def _init_toolbars(self): + """ Setup the 3 vertical toolbars on the main screen """ + self._setup_from_toolbar() + self._setup_copy_move_toolbar() + self._setup_dest_toolbar() + + # size label widths so they are equal, or else the left border of the file chooser will not match + self.photo_dest_label.realize() + self._make_widget_widths_equal(self.photo_dest_label, self.video_dest_label) + self.photo_dest_label.set_alignment(xalign=0.0, yalign=0.5) + self.video_dest_label.set_alignment(xalign=0.0, yalign=0.5) + + # size copy / move buttons so they are equal in length, so arrows align + self._make_widget_widths_equal(self.copy_button, self.move_button) + + def _setup_from_toolbar(self): + self.from_toolbar.set_style(gtk.TOOLBAR_TEXT) + self.from_toolbar.set_border_width(5) + + from_label = gtk.Label() + from_label.set_markup("" + _("From") + "") + self.from_toolbar_label = gtk.ToolItem() + self.from_toolbar_label.add(from_label) + self.from_toolbar_label.set_is_important(True) + self.from_toolbar.insert(self.from_toolbar_label, 0) + + self.auto_detect_button = gtk.ToggleToolButton() + self.auto_detect_button.set_is_important(True) + self.auto_detect_button.set_label(_("Auto Detect")) + self.from_toolbar.insert(self.auto_detect_button, 1) + + self.from_filechooser_button = gtk.FileChooserButton( + _("Select a folder containing %(file_types)s") % {'file_types':file_types_to_download()}) + self.from_filechooser_button.set_action( + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + + self.from_filechooser = gtk.ToolItem() + self.from_filechooser.set_is_important(True) + self.from_filechooser.add(self.from_filechooser_button) + self.from_filechooser.set_expand(True) + self.from_toolbar.insert(self.from_filechooser, 2) + + self._set_from_toolbar_state() + + #set events after having initialized the values + self.auto_detect_button.connect("toggled", self.on_auto_detect_button_toggled_event) + self.from_filechooser_button.connect("selection-changed", + self.on_from_filechooser_button_selection_changed) + + self.from_toolbar.show_all() + + def _setup_copy_move_toolbar(self): + self.copy_toolbar.set_style(gtk.TOOLBAR_TEXT) + self.copy_toolbar.set_border_width(5) + + copy_move_label = gtk.Label(" ") + self.copy_move_toolbar_label = gtk.ToolItem() + self.copy_move_toolbar_label.add(copy_move_label) + self.copy_move_toolbar_label.set_is_important(True) + self.copy_toolbar.insert(self.copy_move_toolbar_label, 0) + + self.copy_hbox = gtk.HBox() + self.move_hbox = gtk.HBox() + self.forward_image = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_SMALL_TOOLBAR) + self.forward_image2 = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_SMALL_TOOLBAR) + self.forward_image3 = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_SMALL_TOOLBAR) + self.forward_image4 = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_SMALL_TOOLBAR) + self.forward_label = gtk.Label(" ") + self.forward_label2 = gtk.Label(" ") + self.forward_label3 = gtk.Label(" ") + self.forward_label4 = gtk.Label(" ") + + self.copy_button = gtk.RadioToolButton() + self.copy_button.set_label(_("Copy")) + self.copy_button.set_is_important(True) + + self.copy_hbox.pack_start(self.forward_label) + self.copy_hbox.pack_start(self.forward_image) + self.copy_hbox.pack_start(self.copy_button, expand=False, fill=False) + self.copy_hbox.pack_start(self.forward_image2) + self.copy_hbox.pack_start(self.forward_label2) + copy_box = gtk.ToolItem() + copy_box.add(self.copy_hbox) + self.copy_toolbar.insert(copy_box, 1) + + self.move_button = gtk.RadioToolButton(self.copy_button) + self.move_button.set_label(_("Move")) + self.move_button.set_is_important(True) + self.move_hbox.pack_start(self.forward_label3) + self.move_hbox.pack_start(self.forward_image3) + self.move_hbox.pack_start(self.move_button, expand=False, fill=False) + self.move_hbox.pack_start(self.forward_image4) + self.move_hbox.pack_start(self.forward_label4) + move_box = gtk.ToolItem() + move_box.add(self.move_hbox) + self.copy_toolbar.insert(move_box, 2) + + self.move_button.set_active(self.prefs.auto_delete) + self.copy_button.connect("toggled", self.on_copy_button_toggle_event) + + self.copy_toolbar.show_all() + self._set_copy_toolbar_active_arrows() + + def _setup_dest_toolbar(self): + #Destination Toolbar + self.dest_toolbar.set_border_width(5) + + dest_label = gtk.Label() + dest_label.set_markup("" + _("To") + "") + self.dest_toolbar_label = gtk.ToolItem() + self.dest_toolbar_label.add(dest_label) + self.dest_toolbar_label.set_is_important(True) + self.dest_toolbar.insert(self.dest_toolbar_label, 0) + + photo_dest_hbox = gtk.HBox() + self.photo_dest_label = gtk.Label(_("Photos:")) + + self.to_photo_filechooser_button = gtk.FileChooserButton( + _("Select a folder to download photos to")) + self.to_photo_filechooser_button.set_action( + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + photo_dest_hbox.pack_start(self.photo_dest_label, expand=False, fill=False, padding=6) + photo_dest_hbox.pack_start(self.to_photo_filechooser_button) + self.to_photo_filechooser = gtk.ToolItem() + self.to_photo_filechooser.set_is_important(True) + self.to_photo_filechooser.set_expand(True) + self.to_photo_filechooser.add(photo_dest_hbox) + self.dest_toolbar.insert(self.to_photo_filechooser, 1) + + video_dest_hbox = gtk.HBox() + self.video_dest_label = gtk.Label(_("Videos:")) + self.to_video_filechooser_button = gtk.FileChooserButton( + _("Select a folder to download videos to")) + self.to_video_filechooser_button.set_action( + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + video_dest_hbox.pack_start(self.video_dest_label, expand=False, fill=False, padding=6) + video_dest_hbox.pack_start(self.to_video_filechooser_button) + self.to_video_filechooser = gtk.ToolItem() + self.to_video_filechooser.set_is_important(True) + self.to_video_filechooser.set_expand(True) + self.to_video_filechooser.add(video_dest_hbox) + self.dest_toolbar.insert(self.to_video_filechooser, 2) + + self._set_to_toolbar_values() + self.to_photo_filechooser_button.connect("selection-changed", + self.on_to_photo_filechooser_button_selection_changed) + self.to_video_filechooser_button.connect("selection-changed", + self.on_to_video_filechooser_button_selection_changed) + self.dest_toolbar.show_all() + + def _make_widget_widths_equal(self, widget1, widget2): + """takes two widgets and sets a width for both equal to widest one""" + + x1, y1, w1, h1 = widget1.get_allocation() + x2, y2, w2, h2 = widget2.get_allocation() + w = max(w1, w2) + h = max(h1, h2) + widget1.set_size_request(w,h) + widget2.set_size_request(w,h) + + def _set_copy_toolbar_active_arrows(self): + if self.copy_button.get_active(): + self.forward_image.set_visible(True) + self.forward_image2.set_visible(True) + self.forward_image3.set_visible(False) + self.forward_image4.set_visible(False) + self.forward_label.set_visible(False) + self.forward_label2.set_visible(False) + self.forward_label3.set_visible(True) + self.forward_label4.set_visible(True) + else: + self.forward_image.set_visible(False) + self.forward_image2.set_visible(False) + self.forward_image3.set_visible(True) + self.forward_image4.set_visible(True) + self.forward_label.set_visible(True) + self.forward_label2.set_visible(True) + self.forward_label3.set_visible(False) + self.forward_label4.set_visible(False) + + def on_copy_button_toggle_event(self, radio_button): + self._set_copy_toolbar_active_arrows() + self.prefs.auto_delete = not self.copy_button.get_active() + + def _set_from_toolbar_state(self): + logger.debug("_set_from_toolbar_state") + self.auto_detect_button.set_active(self.prefs.device_autodetection) + if self.prefs.device_autodetection: + self.from_filechooser_button.set_sensitive(False) + self.from_filechooser_button.set_current_folder(self.prefs.device_location) + + def on_auto_detect_button_toggled_event(self, button): + logger.debug("on_auto_detect_button_toggled_event") + self.from_filechooser_button.set_sensitive(not button.get_active()) + if not self.rerun_setup_available_image_and_video_media: + self.prefs.device_autodetection = button.get_active() + + def on_from_filechooser_button_selection_changed(self, filechooserbutton): + logger.debug("on_from_filechooser_button_selection_changed") + path = filechooserbutton.get_current_folder() + if path and not self.rerun_setup_available_image_and_video_media: + self.prefs.device_location = path + + def on_to_photo_filechooser_button_selection_changed(self, filechooserbutton): + path = filechooserbutton.get_current_folder() + if path: + self.prefs.download_folder = path + + def on_to_video_filechooser_button_selection_changed(self, filechooserbutton): + path = filechooserbutton.get_current_folder() + if path: + self.prefs.video_download_folder = path + + def _set_to_toolbar_values(self): + self.to_photo_filechooser_button.set_current_folder(self.prefs.download_folder) + self.to_video_filechooser_button.set_current_folder(self.prefs.video_download_folder) + + def toolbar_event(self, widget, toolbar): + pass + + def _set_window_size(self): """ @@ -3027,7 +3387,7 @@ class RapidApp(dbus.service.Object): Set the size of the device collection scrolled window widget """ if self.device_collection.map_process_to_row: - height = self.device_collection_viewport.size_request()[1] + height = max(self.device_collection_viewport.size_request()[1], 24) self.device_collection_scrolledwindow.set_size_request(-1, height) self.main_vpaned.set_position(height) else: @@ -3407,7 +3767,7 @@ class RapidApp(dbus.service.Object): Set up process managers. A task such as scanning a device or copying files is handled in its - own process. + own process. """ self.batch_size = 10 @@ -3421,19 +3781,27 @@ class RapidApp(dbus.service.Object): self.uses_stored_sequence_no_value, self.uses_session_sequece_no_value, self.uses_sequence_letter_value) - + + # daemon process to rename files and create subfolders self.subfolder_file_manager = SubfolderFileManager( self.subfolder_file_results, - sequence_values, - self.focal_length) - + sequence_values) + # process to scan source devices / paths self.scan_manager = ScanManager(self.scan_results, self.batch_size, self.device_collection.add_device) + + #process to copy files from source to destination self.copy_files_manager = CopyFilesManager(self.copy_files_results, self.batch_size_MB) + + #process to back files up self.backup_manager = BackupFilesManager(self.backup_results, self.batch_size_MB) + + #process to enhance files after they've been copied and before they're + #renamed + self.file_modify_manager = FileModifyManager(self.file_modify_results) def scan_results(self, source, condition): @@ -3468,6 +3836,7 @@ class RapidApp(dbus.service.Object): self.start_download(scan_pid=scan_pid) self.set_thumbnail_sort() + self.download_button.grab_focus() # signal that no more data is coming, finishing io watch for this pipe return False @@ -3482,7 +3851,6 @@ class RapidApp(dbus.service.Object): # must return True for this method to be called again return True - @dbus.service.method (config.DBUS_NAME, in_signature='', out_signature='b') @@ -3511,7 +3879,7 @@ def start(): parser.add_option("-q", "--quiet", action="store_false", dest="verbose", help=_("only output errors to the command line")) # image file extensions are recognized RAW files plus TIFF and JPG parser.add_option("-e", "--extensions", action="store_true", dest="extensions", help=_("list photo and video file extensions the program recognizes and exit")) - parser.add_option("--focal-length", type=int, dest="focal_length", help="If an aperture value of 0.0 is encountered, for file renaming purposes the metadata for that photo will temporarily have its focal length set to the number passed, and its aperture to f8") + parser.add_option("--focal-length", type=int, dest="focal_length", help="If an aperture value of 0.0 is encountered, the focal length metadata will be set to the number passed, and its aperture metadata to f8") parser.add_option("--reset-settings", action="store_true", dest="reset", help=_("reset all program settings and preferences and exit")) (options, args) = parser.parse_args() @@ -3525,7 +3893,7 @@ def start(): logger.setLevel(logging_level) if options.extensions: - extensions = ((rpdfile.RAW_FILE_EXTENSIONS + rpdfile.NON_RAW_IMAGE_FILE_EXTENSIONS, _("Photos:")), (rpdfile.VIDEO_FILE_EXTENSIONS, _("Videos:"))) + extensions = ((rpdfile.PHOTO_EXTENSIONS, _("Photos:")), (rpdfile.VIDEO_EXTENSIONS, _("Videos:"))) for exts, file_type in extensions: v = '' for e in exts[:-1]: @@ -3549,14 +3917,16 @@ def start(): logger.info("Rapid Photo Downloader %s", utilities.human_readable_version(config.version)) logger.info("Using pyexiv2 %s", metadataphoto.pyexiv2_version_info()) logger.info("Using exiv2 %s", metadataphoto.exiv2_version_info()) - + if metadataexiftool.EXIFTOOL_VERSION is None: + logger.info("Exiftool not detected") + else: + logger.info("Using exiftool %s", metadataexiftool.EXIFTOOL_VERSION) + if metadatavideo.HAVE_HACHOIR: + logger.info("Using hachoir %s", metadatavideo.version_info()) + + if focal_length: logger.info("Focal length of %s will be used when an aperture of 0.0 is encountered", focal_length) - - if DOWNLOAD_VIDEO: - logger.info("Using hachoir %s", metadatavideo.version_info()) - else: - logger.info(_("Video downloading functionality disabled.\nTo download videos, please install the hachoir metadata and kaa metadata packages for python.")) bus = dbus.SessionBus () request = bus.request_name (config.DBUS_NAME, dbus.bus.NAME_FLAG_DO_NOT_QUEUE) diff --git a/rapid/rpdfile.py b/rapid/rpdfile.py index d9454d7..8818182 100644 --- a/rapid/rpdfile.py +++ b/rapid/rpdfile.py @@ -34,6 +34,7 @@ from gettext import gettext as _ import config import metadataphoto import metadatavideo +import metadataexiftool import problemnotification as pn @@ -43,7 +44,9 @@ import thumbnail as tn RAW_EXTENSIONS = ['arw', 'dcr', 'cr2', 'crw', 'dng', 'mos', 'mef', 'mrw', 'nef', 'orf', 'pef', 'raf', 'raw', 'rw2', 'sr2', 'srw'] -NON_RAW_IMAGE_EXTENSIONS = ['jpg', 'jpe', 'jpeg', 'tif', 'tiff'] +JPEG_EXTENSIONS = ['jpg', 'jpe', 'jpeg'] + +NON_RAW_IMAGE_EXTENSIONS = JPEG_EXTENSIONS + ['tif', 'tiff'] PHOTO_EXTENSIONS = RAW_EXTENSIONS + NON_RAW_IMAGE_EXTENSIONS @@ -52,7 +55,9 @@ if metadatavideo.DOWNLOAD_VIDEO: # needs to be able to download videos VIDEO_EXTENSIONS = ['3gp', 'avi', 'm2t', 'mov', 'mp4', 'mpeg','mpg', 'mod', 'tod'] - VIDEO_THUMBNAIL_EXTENSIONS = ['thm'] + if metadataexiftool.EXIFTOOL_VERSION is not None: + VIDEO_EXTENSIONS += ['mts'] + VIDEO_THUMBNAIL_EXTENSIONS = ['thm'] else: VIDEO_EXTENSIONS = [] VIDEO_THUMBNAIL_EXTENSIONS = [] @@ -75,18 +80,18 @@ def file_type(file_extension): return None def get_rpdfile(extension, name, display_name, path, size, - file_system_modification_time, - scan_pid, file_id): + file_system_modification_time, thm_full_name, + scan_pid, file_id, file_type): - if extension in VIDEO_EXTENSIONS: + if file_type == FILE_TYPE_VIDEO: return Video(name, display_name, path, size, - file_system_modification_time, + file_system_modification_time, thm_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, + file_system_modification_time, thm_full_name, scan_pid, file_id) class FileTypeCounter: @@ -144,7 +149,7 @@ class RPDFile: """ def __init__(self, name, display_name, path, size, - file_system_modification_time, + file_system_modification_time, thm_full_name, scan_pid, file_id): self.path = path @@ -153,11 +158,15 @@ class RPDFile: self.display_name = display_name 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 + self.status = config.STATUS_NOT_DOWNLOADED self.problem = None # class Problem in problemnotifcation.py @@ -177,12 +186,18 @@ class RPDFile: # generated values self.temp_full_file_name = '' + self.temp_thm_full_name = '' + self.temp_xmp_full_name = '' + self.download_start_time = None self.download_subfolder = '' self.download_path = '' self.download_name = '' - self.download_full_file_name = '' + self.download_full_file_name = '' #file name with path + 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.metadata = None @@ -194,11 +209,24 @@ class RPDFile: #self.subfolder_pref_list = [] #self.name_pref_list = [] #strip_characters = False + #self.thm_extension = '' + #self.xmp_extension = '' + + #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 + def initialize_problem(self): self.problem = pn.Problem() # these next values are used to display in the error log window @@ -218,32 +246,6 @@ class RPDFile: def add_extra_detail(self, extra_detail, *args): self.problem.add_extra_detail(extra_detail, *args) - - - -#~ exif_tags_needed = ('Exif.Photo.FNumber', - #~ 'Exif.Photo.ISOSpeedRatings', - #~ 'Exif.Photo.ExposureTime', - #~ 'Exif.Photo.FocalLength', - #~ 'Exif.Image.Make', - #~ 'Exif.Image.Model', - #~ 'Exif.Canon.SerialNumber', - #~ 'Exif.Nikon3.SerialNumber' - #~ 'Exif.OlympusEq.SerialNumber', - #~ 'Exif.Olympus.SerialNumber', - #~ 'Exif.Olympus.SerialNumber2', - #~ 'Exif.Panasonic.SerialNumber', - #~ 'Exif.Fujifilm.SerialNumber', - #~ 'Exif.Image.CameraSerialNumber', - #~ 'Exif.Nikon3.ShutterCount', - #~ 'Exif.Canon.FileNumber', - #~ 'Exif.Canon.ImageNumber', - #~ 'Exif.Canon.OwnerName', - #~ 'Exif.Photo.DateTimeOriginal', - #~ 'Exif.Image.DateTime', - #~ 'Exif.Photo.SubSecTimeOriginal', - #~ 'Exif.Image.Orientation' - #~ ) class Photo(RPDFile): @@ -253,9 +255,8 @@ class Photo(RPDFile): def _assign_file_type(self): self.file_type = FILE_TYPE_PHOTO - def load_metadata(self): - - self.metadata = metadataphoto.MetaData(self.full_file_name) + def load_metadata(self, temp_file=False): + self.metadata = metadataphoto.MetaData(self._load_file_for_metadata(temp_file)) try: self.metadata.read() except: @@ -273,8 +274,13 @@ class Video(RPDFile): def _assign_file_type(self): self.file_type = FILE_TYPE_VIDEO - def load_metadata(self): - self.metadata = metadatavideo.VideoMetaData(self.full_file_name) + def load_metadata(self, temp_file=False): + if self.extension == 'mts' or not metadatavideo.HAVE_HACHOIR: + if metadatavideo.HAVE_HACHOIR: + logger.debug("Using ExifTool parser") + self.metadata = metadataexiftool.ExifToolMetaData(self._load_file_for_metadata(temp_file)) + else: + self.metadata = metadatavideo.VideoMetaData(self._load_file_for_metadata(temp_file)) return True class SamplePhoto(Photo): @@ -285,7 +291,8 @@ class SamplePhoto(Photo): size=23516764, file_system_modification_time=time.time(), scan_pid=2033, - file_id='9873afe') + file_id='9873afe', + thm_full_name=None) self.sequences = sequences self.metadata = metadataphoto.DummyMetaData() self.download_start_time = datetime.datetime.now() @@ -298,7 +305,8 @@ class SampleVideo(Video): size=823513764, file_system_modification_time=time.time(), scan_pid=2033, - file_id='9873qrsfe') + file_id='9873qrsfe', + thm_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 0d7182c..02902ec 100755 --- a/rapid/scan.py +++ b/rapid/scan.py @@ -41,6 +41,24 @@ 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. + """ + + f = None + for e in rpdfile.VIDEO_THUMBNAIL_EXTENSIONS: + 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 class Scan(multiprocessing.Process): @@ -120,7 +138,8 @@ class Scan(multiprocessing.Process): return None elif file_type == gio.FILE_TYPE_REGULAR: - ext = os.path.splitext(name)[1].lower()[1:] + base_name, ext = os.path.splitext(name) + ext = ext.lower()[1:] file_type = rpdfile.file_type(ext) if file_type is not None: @@ -134,15 +153,24 @@ class Scan(multiprocessing.Process): display_name = child.get_display_name() 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, - path.get_path(), + path_name, size, - modification_time, + modification_time, + thm_full_name, self.pid, - file_id) + file_id, + file_type) self.files.append(scanned_file) diff --git a/rapid/subfolderfile.py b/rapid/subfolderfile.py index a80bc84..345edce 100644 --- a/rapid/subfolderfile.py +++ b/rapid/subfolderfile.py @@ -20,10 +20,10 @@ """ Generates names for files and folders. -Runs a daemon process. +Runs as a daemon process. """ -import os, datetime, collections, fractions +import os, datetime, collections import gio import multiprocessing @@ -82,13 +82,17 @@ def time_subseconds_human_readable(date, subseconds): 'second':date.strftime("%S"), 'subsecond': subseconds} -def load_metadata(rpd_file): +def load_metadata(rpd_file, temp_file=True): """ Loads the metadata for the file. Returns True if operation succeeded, false otherwise + + If temp_file is true, the the metadata from the temporary file rather than + the original source file is used. This is important, because the metadata + can be modified by the filemodify process. """ if rpd_file.metadata is None: - if not rpd_file.load_metadata(): + if not rpd_file.load_metadata(temp_file): # Error in reading metadata rpd_file.add_problem(None, pn.CANNOT_DOWNLOAD_BAD_METADATA, {'filetype': rpd_file.title_capitalized}) return False @@ -133,10 +137,10 @@ def generate_name(rpd_file): rpd_file.download_name = _generate_name(generator, rpd_file) return rpd_file - + class SubfolderFile(multiprocessing.Process): - def __init__(self, results_pipe, sequence_values, focal_length): + def __init__(self, results_pipe, sequence_values): multiprocessing.Process.__init__(self) self.daemon = True self.results_pipe = results_pipe @@ -150,8 +154,6 @@ class SubfolderFile(multiprocessing.Process): self.uses_session_sequece_no = sequence_values[6] self.uses_sequence_letter = sequence_values[7] - self.focal_length = focal_length - logger.debug("Start of day is set to %s", self.day_start.value) def progress_callback_no_update(self, amount_downloaded, total): @@ -237,6 +239,7 @@ class SubfolderFile(multiprocessing.Process): """ Get subfolder and name. Attempt to move the file from it's temporary directory. + Move video THM file if there is one. If successful, increment sequence values. Report any success or failure. """ @@ -265,10 +268,8 @@ class SubfolderFile(multiprocessing.Process): while True: logger.debug("Finished %s. Getting next task.", download_count) - task = self.results_pipe.recv() - # rename file and move to generated subfolder - download_succeeded, download_count, rpd_file = task + download_succeeded, download_count, rpd_file = self.results_pipe.recv() move_succeeded = False @@ -311,22 +312,16 @@ class SubfolderFile(multiprocessing.Process): # Generate subfolder name and new file name generation_succeeded = True - # check to see if focal length and aperture data should be manipulated - if self.focal_length is not None and rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: - if load_metadata(rpd_file): - a = rpd_file.metadata.aperture() - if a == '0.0': - fl = rpd_file.metadata["Exif.Photo.FocalLength"].value - logger.info("Adjusting focal length and aperture for %s", rpd_file.full_file_name) - #~ try: - rpd_file.metadata["Exif.Photo.FocalLength"] = fractions.Fraction(self.focal_length,1) - rpd_file.metadata["Exif.Photo.FNumber"] = fractions.Fraction(8,1) - #~ rpd_file.metadata.write(preserve_timestamps=True) - #~ logger.info("...wrote new value") - #~ except: - #~ logger.error("failed to write value!") - - + if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: + if hasattr(rpd_file, 'new_focal_length'): + # A RAW file has had its focal length and aperture adjusted. + # These have been written out to an XMP sidecar, but they won't + # be picked up by pyexiv2. So temporarily change the values inplace here, + # without saving them. + if load_metadata(rpd_file): + rpd_file.metadata["Exif.Photo.FocalLength"] = rpd_file.new_focal_length + rpd_file.metadata["Exif.Photo.FNumber"] = rpd_file.new_aperture + rpd_file = generate_subfolder(rpd_file) if rpd_file.download_subfolder: @@ -374,6 +369,7 @@ class SubfolderFile(multiprocessing.Process): if generation_succeeded: rpd_file.download_path = os.path.join(rpd_file.download_folder, rpd_file.download_subfolder) rpd_file.download_full_file_name = os.path.join(rpd_file.download_path, rpd_file.download_name) + rpd_file.download_full_base_name = os.path.splitext(rpd_file.download_full_file_name)[0] subfolder = gio.File(path=rpd_file.download_path) @@ -468,6 +464,36 @@ class SubfolderFile(multiprocessing.Process): self.downloads_today_tracker.increment_downloads_today() self.downloads_today.value = self.downloads_today_tracker.get_raw_downloads_today() self.downloads_today_date.value = self.downloads_today_tracker.get_raw_downloads_today_date() + + if rpd_file.temp_thm_full_name: + # copy and rename THM video file + source = gio.File(path=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 + 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) + + if rpd_file.temp_xmp_full_name: + # copy and rename XMP sidecar file + source = gio.File(path=rpd_file.temp_xmp_full_name) + # generate_name() has generated xmp extension with correct capitalization + download_xmp_full_name = rpd_file.download_full_base_name + rpd_file.xmp_extension + dest = gio.File(path=download_xmp_full_name) + try: + source.move(dest, self.progress_callback_no_update, cancellable=None) + rpd_file.download_xmp_full_name = download_xmp_full_name + except gio.Error, inst: + logger.error("Failed to move XMP sidecar file %s", download_xmp_full_name) + if not move_succeeded: logger.error("%s: %s - %s", rpd_file.full_file_name, diff --git a/rapid/thumbnail.py b/rapid/thumbnail.py index c987018..404c18b 100644 --- a/rapid/thumbnail.py +++ b/rapid/thumbnail.py @@ -1,5 +1,4 @@ #!/usr/bin/python -#!/usr/bin/python # -*- coding: latin1 -*- ### Copyright (C) 2011 Damon Lynch @@ -137,26 +136,6 @@ class PicklablePIL: def get_pixbuf(self): return image_to_pixbuf(self.get_image()) -def get_video_THM_file(fullFileName): - """ - 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. - """ - - f = None - name, ext = os.path.splitext(fullFileName) - for e in rpdfile.VIDEO_THUMBNAIL_EXTENSIONS: - if os.path.exists(name + '.' + e): - f = name + '.' + e - break - if os.path.exists(name + '.' + e.upper()): - f = name + '.' + e.upper() - break - - return f - class Thumbnail: # file types from which to remove letterboxing (black bands in the thumbnail @@ -187,17 +166,24 @@ class Thumbnail: return (thumbnail.data, lowrez) def _process_thumbnail(self, image, size_reduced): + image_ok = True if image.mode <> "RGBA": - image = image.convert("RGBA") + try: + image = image.convert("RGBA") + except: + logger.error("Image thumbnail is corrupt") + image_ok = False - thumbnail = PicklablePIL(image) - if size_reduced is not None: - thumbnail_icon = image.copy() - downsize_pil(thumbnail_icon, size_reduced, fit=False) - thumbnail_icon = PicklablePIL(thumbnail_icon) + if image_ok: + thumbnail = PicklablePIL(image) + if size_reduced is not None: + thumbnail_icon = image.copy() + downsize_pil(thumbnail_icon, size_reduced, fit=False) + thumbnail_icon = PicklablePIL(thumbnail_icon) + else: + thumbnail_icon = None else: - thumbnail_icon = None - + thumbnail = thumbnail_icon = None return (thumbnail, thumbnail_icon) def _get_photo_thumbnail(self, full_file_name, size_max, size_reduced): @@ -263,7 +249,7 @@ class Thumbnail: logger.debug("...got thumbnail for %s", full_file_name) return (thumbnail, thumbnail_icon) - def _get_video_thumbnail(self, full_file_name, size_max, size_reduced): + def _get_video_thumbnail(self, full_file_name, thm_full_name, size_max, size_reduced): thumbnail = None thumbnail_icon = None if size_max is None: @@ -272,14 +258,16 @@ class Thumbnail: size = max(size_max[0], size_max[1]) image = None if size > 0 and size <= 160: - thm = get_video_THM_file(full_file_name) - if thm: + if thm_full_name: try: - thumbnail = gtk.gdk.pixbuf_new_from_file(thm) + thumbnail = gtk.gdk.pixbuf_new_from_file(thm_full_name) except: - logger.warning("Could not open THM file for %s", full_file_name) - thumbnail = add_filmstrip(thumbnail) - image = pixbuf_to_image(thumbnail) + logger.error("Could not open THM file for %s", full_file_name) + logger.error("Thumbnail file is %s", thm_full_name) + image = None + else: + thumbnail = add_filmstrip(thumbnail) + image = pixbuf_to_image(thumbnail) if image is None: try: @@ -299,13 +287,13 @@ class Thumbnail: logger.debug("...got thumbnail for %s", full_file_name) return (thumbnail, thumbnail_icon) - def get_thumbnail(self, full_file_name, file_type, size_max=None, size_reduced=None): + def get_thumbnail(self, full_file_name, thm_full_name, file_type, size_max=None, size_reduced=None): logger.debug("Getting thumbnail for %s...", full_file_name) if file_type == rpdfile.FILE_TYPE_PHOTO: logger.debug("file type is photo") return self._get_photo_thumbnail(full_file_name, size_max, size_reduced) else: - return self._get_video_thumbnail(full_file_name, size_max, size_reduced) + return self._get_video_thumbnail(full_file_name, thm_full_name, size_max, size_reduced) class GetPreviewImage(multiprocessing.Process): @@ -332,8 +320,8 @@ class GetPreviewImage(multiprocessing.Process): def run(self): while True: - unique_id, full_file_name, file_type, size_max = self.results_pipe.recv() - full_size_preview, reduced_size_preview = self.thumbnail_maker.get_thumbnail(full_file_name, file_type, size_max=size_max, size_reduced=(100,100)) + unique_id, full_file_name, thm_full_name, file_type, size_max = self.results_pipe.recv() + full_size_preview, reduced_size_preview = self.thumbnail_maker.get_thumbnail(full_file_name, thm_full_name, file_type, size_max=size_max, size_reduced=(100,100)) if full_size_preview is None: full_size_preview = self.get_stock_image(file_type) self.results_pipe.send((unique_id, full_size_preview, reduced_size_preview)) @@ -369,9 +357,9 @@ class GenerateThumbnails(multiprocessing.Process): logger.info("Terminating thumbnailing") return None - thumbnail, thumbnail_icon = self.thumbnail_maker.get_thumbnail( f.full_file_name, + f.thm_full_name, f.file_type, (160, 120), (100,100)) -- cgit v1.2.3