diff options
Diffstat (limited to 'rapid')
-rw-r--r-- | rapid/ChangeLog | 84 | ||||
-rw-r--r-- | rapid/backupfile.py | 5 | ||||
-rw-r--r-- | rapid/config.py | 2 | ||||
-rw-r--r-- | rapid/device.py | 18 | ||||
-rw-r--r-- | rapid/downloadtracker.py | 50 | ||||
-rw-r--r-- | rapid/glade3/about.ui | 9 | ||||
-rw-r--r-- | rapid/glade3/prefs.ui | 471 | ||||
-rw-r--r-- | rapid/preferencesdialog.py | 330 | ||||
-rw-r--r-- | rapid/prefsrapid.py | 38 | ||||
-rwxr-xr-x | rapid/problemnotification.py | 2 | ||||
-rwxr-xr-x | rapid/rapid.py | 389 | ||||
-rwxr-xr-x | rapid/scan.py | 47 | ||||
-rw-r--r-- | rapid/subfolderfile.py | 13 | ||||
-rw-r--r-- | rapid/thumbnail.py | 3 |
14 files changed, 1270 insertions, 191 deletions
diff --git a/rapid/ChangeLog b/rapid/ChangeLog index 7c74bb9..35cd727 100644 --- a/rapid/ChangeLog +++ b/rapid/ChangeLog @@ -1,12 +1,64 @@ +Version 0.4.2 +------------- + +2011-10-01 + +Added feature in Preferences window to remove any paths that have previously +been marked to always be scanned or ignored. These paths can be specified when +automatic detection of Portable Storage Devices is enabled. + +Fixed bug #768026: added option to ignore paths from which to download - + +You can now specify paths never to scan for photos or videos. By default, any +path ending in .Trash or .thumbnails is ignored. Advanced users can specify +paths to never scan using python-style regular expressions. + +Fixed bug #774488: added manual backup path for videos, in addition to photos - + +You can now manually specify a path specifically in which to backup videos. This +can be the same as or different than the path in which to backup photos. + +Fixed bug #838722: wrong file types may be backed up to external devices - + +Fixed a bug when auto detection of backup devices is enabled, files of the wrong +type might be backed up. For instance, if the backup device is only meant to +store videos, and the download contains photos, photos would incorrectly be +backed up to the device in addition to videos. + +Fixed bug #815727: Backup errors and warnings incorrectly displayed in log +window - + +Fixed a bug that occurred when backing up errors are encountered, the log window +did not display them correctly, although they were correctly outputted to the +terminal window. This only occurred when more than one backup device was being +used during a download. + +Fixed bug #859242: Crash when displaying a preview of file without an extracted +thumbnail. + +Fixed bug #810559: Crash when generating thumbnail images + +Fixed bug #789995: crash when --reset-settings option is given on the command +line. + +Fixed bugs #795446 and #844714: small errors in translation template. + +Fixed a bug in the Swedish translation. + +Added Danish translation, by Torben Gundtofte-Bruun. Updated Brazilian, Czech, +Dutch, French, German, Hungarian, Italian, Japanese, Norwegian, Polish, Russian, +Serbian, Slovak, Spanish, Swedish and Turkish translations. + + Version 0.4.1 ------------- 2011-05-19 -Added exif Artist and Copyright metadata options to file and subfoler name +Added exif Artist and Copyright metadata options to file and subfolder name generation. -Fixed bug #774476: thumbnails occassionally not sorted by file modification +Fixed bug #774476: thumbnails occasionally not sorted by file modification time. Fixed bug #784399: job code not prompted for after preference change. @@ -26,7 +78,7 @@ Version 0.4.0 2011-04-28 -Features added since Release Candiate 1: +Features added since Release Candidate 1: * Allow multiple selection of files to check or uncheck for downloading. * Automation feature to delete downloaded files from a device. @@ -174,7 +226,7 @@ download. It also performs the actual downloads quicker. It will use multiple CPU cores if they are available. Rapid Photo Downloader now requires version 0.3.0 or newer of pyexiv2. It also -requries Python Imaging (PIL) to run. It will only run on recent Linux +requires Python Imaging (PIL) to run. It will only run on recent Linux distributions such as Ubuntu 10.04 or newer. It has been tested on Ubuntu 10.04, 10.10 and 11.04, as well as Fedora 14. (There is currently an unusual bug adjusting some preferences when running Ubuntu 11.04. See bug #739021). @@ -210,7 +262,7 @@ Version 0.3.5 2011-03-23 -The primrary purpose of this release is update translations and fix bug #714039, +The primary purpose of this release is update translations and fix bug #714039, where under certain circumstances the program could crash while downloading files. @@ -265,7 +317,7 @@ is that the video creation date is now correctly read (the creation time read by kaa metadata was sometimes wrong by a few hours). Kaa-metadata is still used to extract some the codec, fourcc and frames per second (FPS) metadata. -Fixed bug #640722: Added preliminary support for Samusung SRW files. Current +Fixed bug #640722: Added preliminary support for Samsung SRW files. Current versions of Exiv2 and pyexiv2 can read some but not all metadata from this new RAW format. If you try to use metadata that cannot be extracted, Rapid Photo Downloader will issue a warning. @@ -291,7 +343,7 @@ Version 0.3.2 Added Norwegian Nynorsk translation. Updated Chinese, Finnish, Hungarian, Dutch, Occitan (post 1500), Polish, Brazilian Portuguese, and Russian translations. -Fixed crash on startup when checking for freespace, and the download folder does +Fixed crash on startup when checking for free space, and the download folder does not exist. @@ -304,7 +356,7 @@ The main window now works more effectively on tiny screens, such as those found on netbooks. If the screen height is less than or equal to 650 pixels, elements in the preview pane are removed, and the spacing is tightened. -The amount of free space available on the filesystem where photos are to be +The amount of free space available on the file-system where photos are to be downloaded is now displayed in the status bar. (Note this is only the case on moderately up-to-date Linux distributions that use GVFS, such as Ubuntu 8.10 or higher). @@ -405,7 +457,7 @@ Don't stop a file being downloaded if a valid subfolder or filename can be generated using a Job Code. Bug fix: don't automatically exit if there were errors or warnings and a -download was occuring from more than one device. +download was occurring from more than one device. Auto start now works correctly again. @@ -534,7 +586,7 @@ system has, it must either be running pyexiv2 >= 0.2.0, or have exiv2 installed. Fixed bug #483222: sometimes images could not be downloaded to NTFS partitions. This fix was a welcome side effect of using GIO to copy images, instead of -relying on the python standard libary. +relying on the python standard library. Error message headings in the Error Log are now displayed in a red font. @@ -568,7 +620,7 @@ dialog window. Fixed bug #510484: Crashes when fails to create temporary download directory. Fixed bug #510516: Program now checks to see if the download folder exists and -is writeable. If automatic detection of image devices is not enabled, it checks +is writable. If automatic detection of image devices is not enabled, it checks to see if the image location path exists. Updated Czech, Dutch, Finnish, French, German, Hungarian, Italian, Polish, @@ -591,7 +643,7 @@ example, if 200 RAW images and 200 matching JPEG images are downloaded, the value of Downloads today will be incremented by 200, and not 400. The same goes for the rest of the sequence values, including the Stored number sequence number. Images are detected by comparing filename, as well as the exif value for -the date and time the image was created (including subseconds when the camera +the date and time the image was created (including sub seconds when the camera records this value). This option will take effect regardless of whether the RAW and JPEG images are stored on different memory cards or the same memory card. Furthermore, if they are stored on separate memory cards, you can download from @@ -747,7 +799,7 @@ to the documentation found online at the program's website. This documentation is now complete. The Preferences Dialog Window is now navigated using a list control, as it was -in early versions of the program. This change was necesseary because with some +in early versions of the program. This change was necessary because with some translations, the dialog window was becoming too wide with the normal tab layout. Usability of the preferences dialog is improved: it will now resize itself based on its content. @@ -902,7 +954,7 @@ more helpful information is printed to the console output. Bug fix: better handle automated shortening Canon names like 'Canon 5D Mark II'. It is now shortened to '5DMkII' instead of merely '5D'. -Bug fix: reenable example of image renaming and subfolder name generation by +Bug fix: re-enable example of image renaming and subfolder name generation by using first image from the first available download device. This was inadvertently disabled in an earlier beta. @@ -975,7 +1027,7 @@ Version 0.0.8 beta 2 2009-03-25 -First Ububtu package. +First Ubuntu package. Rename tarball package to suit package name. @@ -1020,7 +1072,7 @@ robust. Remove list control from preferences, reverting to normal tabbed preferences, as the window was becoming too wide. -Show notifcations via libnotify. +Show notifications via libnotify. Error and warning icons can now be clicked on to open log window. diff --git a/rapid/backupfile.py b/rapid/backupfile.py index 6b6d11d..40039bc 100644 --- a/rapid/backupfile.py +++ b/rapid/backupfile.py @@ -31,6 +31,9 @@ import rpdfile import problemnotification as pn import config +PHOTO_BACKUP = 1 +VIDEO_BACKUP = 2 +PHOTO_VIDEO_BACKUP = 3 from gettext import gettext as _ @@ -149,7 +152,7 @@ class BackupFiles(multiprocessing.Process): if backup_duplicate_overwrite: flags = gio.FILE_COPY_OVERWRITE else: - flags = gio.FILE_COPY_NONE + flags = gio.FILE_COPY_NONE try: source.copy(dest, self.progress_callback, flags, diff --git a/rapid/config.py b/rapid/config.py index ce498c0..47ecfff 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.1' +version = '0.4.2' GCONF_KEY="/apps/rapid-photo-downloader" diff --git a/rapid/device.py b/rapid/device.py index dcfdf94..423acdd 100644 --- a/rapid/device.py +++ b/rapid/device.py @@ -156,21 +156,5 @@ def is_DCIM_device(path): test_path = os.path.join(path, "DCIM") return utilities.is_directory(test_path) -def is_backup_media(path, identifiers, writeable=True): - """ Test to see if path is used as a backup medium for storing photos or videos - - Identifiers is expected to be a list of folder names to check to see - if the path is a backup path. Only one of them needs to be present - for the path to be considered a backup medium. - - If writeable is True, the directory must be writeable by the user """ - suitable = False - - for identifier in identifiers: - if os.path.isdir(os.path.join(path, identifier)): - if writeable: - suitable = os.access(os.path.join(path, identifier), os.W_OK) - else: - suitable = True - return suitable + diff --git a/rapid/downloadtracker.py b/rapid/downloadtracker.py index 824a812..df48754 100644 --- a/rapid/downloadtracker.py +++ b/rapid/downloadtracker.py @@ -40,10 +40,16 @@ class DownloadTracker: def _refresh_values(self): """ these values are reset when a download is completed""" self.size_of_download_in_bytes_by_scan_pid = dict() + self.total_bytes_backed_up_by_scan_pid = dict() + self.size_of_photo_backup_in_bytes_by_scan_pid = dict() + self.size_of_video_backup_in_bytes_by_scan_pid = dict() self.raw_size_of_download_in_bytes_by_scan_pid = dict() self.total_bytes_copied_by_scan_pid = dict() - self.total_bytes_backed_up_by_scan_pid = dict() + self.total_bytes_video_backed_up_by_scan_pid = dict() self.no_files_in_download_by_scan_pid = dict() + self.no_photos_in_download_by_scan_pid = dict() + self.no_videos_in_download_by_scan_pid = dict() + # 'Download count' tracks the index of the file being downloaded # into the list of files that need to be downloaded -- much like @@ -66,11 +72,30 @@ class DownloadTracker: self.backups_performed_by_unique_id = dict() self.auto_delete = dict() - def set_no_backup_devices(self, no_backup_devices): - self.no_backup_devices = no_backup_devices + def set_no_backup_devices(self, no_photo_backup_devices, no_video_backup_devices): + self.no_photo_backup_devices = no_photo_backup_devices + self.no_video_backup_devices = no_video_backup_devices + self.no_backup_devices = no_photo_backup_devices + no_video_backup_devices + + def get_no_backup_devices(self): + """ + Returns how many devices are being used to backup files of each type + Return value is an integer tuple: photo and video + """ + return (self.no_photo_backup_devices, self.no_video_backup_devices) - def init_stats(self, scan_pid, bytes, no_files): + def init_stats(self, scan_pid, photo_size_in_bytes, video_size_in_bytes, no_photos_to_download, no_videos_to_download): + no_files = no_photos_to_download + no_videos_to_download self.no_files_in_download_by_scan_pid[scan_pid] = no_files + self.no_photos_in_download_by_scan_pid[scan_pid] = no_photos_to_download + self.no_videos_in_download_by_scan_pid[scan_pid] = no_videos_to_download + self.size_of_photo_backup_in_bytes_by_scan_pid[scan_pid] = photo_size_in_bytes * self.no_photo_backup_devices + self.size_of_video_backup_in_bytes_by_scan_pid[scan_pid] = video_size_in_bytes * self.no_video_backup_devices + bytes = photo_size_in_bytes + video_size_in_bytes + # rename_chunk is used to account for the time it takes to rename a file + # it is arbitrarily set to 10% of the time it takes to copy it + # this makes a difference to the user when they're downloading from a + # a high speed source self.rename_chunk[scan_pid] = bytes / 10 / no_files self.size_of_download_in_bytes_by_scan_pid[scan_pid] = bytes + self.rename_chunk[scan_pid] * no_files self.raw_size_of_download_in_bytes_by_scan_pid[scan_pid] = bytes @@ -86,7 +111,6 @@ class DownloadTracker: def get_no_files_in_download(self, scan_pid): return self.no_files_in_download_by_scan_pid[scan_pid] - def get_no_files_downloaded(self, scan_pid, file_type): if file_type == FILE_TYPE_PHOTO: return self.photos_downloaded.get(scan_pid, 0) @@ -119,9 +143,12 @@ class DownloadTracker: self.backups_performed_by_unique_id[unique_id] = \ self.backups_performed_by_unique_id.get(unique_id, 0) + 1 - def all_files_backed_up(self, unique_id): + def all_files_backed_up(self, unique_id, file_type): if unique_id in self.backups_performed_by_unique_id: - return self.backups_performed_by_unique_id[unique_id] == self.no_backup_devices + if file_type == FILE_TYPE_PHOTO: + return self.backups_performed_by_unique_id[unique_id] == self.no_photo_backup_devices + else: + return self.backups_performed_by_unique_id[unique_id] == self.no_video_backup_devices else: logger.critical("Unexpected unique_id in self.backups_performed_by_unique_id") return True @@ -155,14 +182,17 @@ class DownloadTracker: has been completed """ - # three components: copy (download), rename, and backup + # when calculating the percentage, there are three components: + # copy (download), rename ('rename_chunk'), and backup percent_complete = (((float( self.total_bytes_copied_by_scan_pid[scan_pid]) + self.rename_chunk[scan_pid] * self.files_downloaded[scan_pid]) + self.total_bytes_backed_up_by_scan_pid[scan_pid]) / (self.size_of_download_in_bytes_by_scan_pid[scan_pid] + - self.raw_size_of_download_in_bytes_by_scan_pid[scan_pid] * - self.no_backup_devices)) * 100 + self.size_of_photo_backup_in_bytes_by_scan_pid[scan_pid] + + self.size_of_video_backup_in_bytes_by_scan_pid[scan_pid] + )) * 100 + return percent_complete def get_overall_percent_complete(self): diff --git a/rapid/glade3/about.ui b/rapid/glade3/about.ui index ef8a65b..3e59698 100644 --- a/rapid/glade3/about.ui +++ b/rapid/glade3/about.ui @@ -1,7 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <interface> <requires lib="gtk+" version="2.20"/> - <!-- interface-naming-policy project-wide --> <object class="GtkAboutDialog" id="about"> <property name="can_focus">False</property> <property name="border_width">5</property> @@ -22,9 +21,13 @@ You should have received a copy of the GNU General Public License along with Rap Lőrincz András <level.andrasnak@gmail.com> Michel Ange <michelange@wanadoo.fr> Alain J. Baudrez <a.baudrez@gmail.com> +Kevin Brubeck Unhammer <unhammer@fsfe.org> Bert <crinbert@yahoo.com> Martin Dahl Moe +Marco de Freitas <marcodefreitas@gmail.com> Martin Egger <martin.egger@gmx.net> +Emanuele Grande <caccolangrifata@gmail.com> +Torben Gundtofte-Bruun <torben@g-b.dk> Miroslav Matejaš <silverspace@ubuntu-hr.org> Nicolás M. Zahlut <nzahlut@live.com> Erik M @@ -49,12 +52,12 @@ Aron Xu <happyaron.xu@gmail.com> <property name="logo">rapid-photo-downloader.svg</property> <property name="wrap_license">True</property> <child internal-child="vbox"> - <object class="GtkVBox" id="dialog-vbox1"> + <object class="GtkBox" id="dialog-vbox1"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="spacing">2</property> <child internal-child="action_area"> - <object class="GtkHButtonBox" id="dialog-action_area1"> + <object class="GtkButtonBox" id="dialog-action_area1"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="layout_style">end</property> diff --git a/rapid/glade3/prefs.ui b/rapid/glade3/prefs.ui index 8f75f62..0a13978 100644 --- a/rapid/glade3/prefs.ui +++ b/rapid/glade3/prefs.ui @@ -1993,8 +1993,10 @@ <property name="can_focus">False</property> <property name="xalign">0</property> <property name="xpad">12</property> - <property name="label" translatable="yes"><b>Devices</b></property> - <property name="use_markup">True</property> + <property name="label" translatable="yes">Devices</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> </object> <packing> <property name="expand">False</property> @@ -2010,9 +2012,9 @@ <property name="xpad">12</property> <property name="label" translatable="yes">Devices are from where to download photos and videos, such as cameras, memory cards or Portable Storage Devices. -You can download photos from multiple devices simultaneously, or you can specify a location on your hard drive. +You can download from multiple devices simultaneously, or you can specify a location on your hard drive. -<i>If downloading directly from your camera works poorly or not at all, try setting it to PTP mode. If that is not possible, consider using a card reader.</i></property> +<i>Downloading directly from cameras is currently an experimental feature. If downloading directly from your camera works poorly or not at all, try setting it to PTP mode. If that is not possible, a card reader must be used.</i></property> <property name="use_markup">True</property> <property name="wrap">True</property> </object> @@ -2083,7 +2085,9 @@ You can download photos from multiple devices simultaneously, or you can specify <property name="can_focus">False</property> <property name="xalign">0</property> <property name="ypad">6</property> - <property name="label" translatable="yes">If you enable automatic detection of Portable Storage Devices, the entire device will be scanned for images. On large devices, this could take some time.</property> + <property name="label" translatable="yes">If you enable automatic detection of Portable Storage Devices, the entire device will be scanned. On large devices, this could take some time. + +When this option is enabled, and a potential device is detected, you will be prompted to determine if it should be scanned or not.</property> <property name="wrap">True</property> </object> <packing> @@ -2158,7 +2162,7 @@ You can download photos from multiple devices simultaneously, or you can specify <property name="xalign">0</property> <property name="xpad">12</property> <property name="ypad">12</property> - <property name="label" translatable="yes">If you disable automatic detection, choose the exact location of the images and videos.</property> + <property name="label" translatable="yes">If you disable automatic detection, choose the exact location of the photos and videos.</property> <property name="wrap">True</property> </object> <packing> @@ -2221,6 +2225,420 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> + <object class="GtkVBox" id="device_options_tab"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkHBox" id="exclusions_header_hbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkImage" id="image10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="stock">gtk-preferences</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="exclusionlabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><span weight="bold" size="x-large">Device Options</span></property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">12</property> + <child> + <object class="GtkVBox" id="vbox15"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="remebered_devices_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="xpad">12</property> + <property name="label" translatable="yes">Remembered Paths</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="remembered_devices_explanation_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="xpad">12</property> + <property name="label" translatable="yes">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.</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="remembered_devices_hbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="remembered_devices_spacer_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="remembered_devices_scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkTreeView" id="remembered_devices_treeview"> + <property name="width_request">250</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="headers_visible">False</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="remembered_devices_button_hbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkVButtonBox" id="remembered_devices_vbuttonbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">12</property> + <property name="layout_style">start</property> + <child> + <object class="GtkButton" id="remove_remembered_device_button"> + <property name="label">gtk-remove</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_action_appearance">False</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_remove_remembered_device_button_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="remove_all_remembered_device_button"> + <property name="label" translatable="yes" comments="The underscore after the C signifies that the l is the accelerator key. This is the standard 'Clear' button, but I needed to change the accelerator from the standard 'c' to 'l' because the close button also used 'c'">R_emove All</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_action_appearance">False</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_remove_all_remembered_device_button_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">12</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="ignored_paths_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="xpad">12</property> + <property name="label" translatable="yes">Ignored Paths</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="ignored_paths_explanation_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="xpad">12</property> + <property name="label" translatable="yes">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.</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="ignored_paths_hbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="ignored_paths_spacer_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="ignored_paths_scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkTreeView" id="ignored_paths_treeview"> + <property name="width_request">250</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="headers_visible">False</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="ignored_paths_button_hbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkVButtonBox" id="ignored_paths_vbuttonbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">12</property> + <property name="layout_style">start</property> + <child> + <object class="GtkButton" id="add_ignored_path_button"> + <property name="label" translatable="yes">_Add...</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_action_appearance">False</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_add_ignored_path_button_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="remove_ignored_path_button"> + <property name="label">gtk-remove</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_action_appearance">False</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_remove_ignored_path_button_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="remove_all_ignored_paths_button"> + <property name="label" translatable="yes" comments="The underscore after the C signifies that the l is the accelerator key. This is the standard 'Clear' button, but I needed to change the accelerator from the standard 'c' to 'l' because the close button also used 'c'">Re_move All</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_action_appearance">False</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_remove_all_ignored_paths_button_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkCheckButton" id="ignored_paths_use_re_checkbutton"> + <property name="label" translatable="yes">Use _python-style regular expressions</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_action_appearance">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_ignored_paths_use_re_checkbutton_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">12</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="position">7</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="device_options_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Device Options</property> + </object> + <packing> + <property name="position">7</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> <object class="GtkVBox" id="backup_tab"> <property name="visible">True</property> <property name="can_focus">False</property> @@ -2321,7 +2739,7 @@ You can download photos from multiple devices simultaneously, or you can specify <object class="GtkTable" id="backup_table"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="n_rows">9</property> + <property name="n_rows">10</property> <property name="n_columns">4</property> <child> <object class="GtkLabel" id="backup_location_explanation_label"> @@ -2330,7 +2748,7 @@ You can download photos from multiple devices simultaneously, or you can specify <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="xalign">0</property> <property name="ypad">12</property> - <property name="label" translatable="yes">If you disable automatic detection, choose the exact backup location.</property> + <property name="label" translatable="yes">If you disable automatic detection, choose the exact backup locations.</property> <property name="wrap">True</property> </object> <packing> @@ -2338,6 +2756,7 @@ You can download photos from multiple devices simultaneously, or you can specify <property name="right_attach">4</property> <property name="top_attach">7</property> <property name="bottom_attach">8</property> + <property name="y_options">GTK_FILL</property> </packing> </child> <child> @@ -2415,7 +2834,7 @@ You can download photos from multiple devices simultaneously, or you can specify <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label" translatable="yes">Backup location:</property> + <property name="label" translatable="yes">Photo backup location:</property> </object> <packing> <property name="left_attach">1</property> @@ -2538,6 +2957,28 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> + <object class="GtkLabel" id="backup_video_location_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Video backup location:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">3</property> + <property name="top_attach">9</property> + <property name="bottom_attach">10</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> <placeholder/> </child> <child> @@ -2608,7 +3049,7 @@ You can download photos from multiple devices simultaneously, or you can specify </child> </object> <packing> - <property name="position">7</property> + <property name="position">8</property> </packing> </child> <child type="tab"> @@ -2618,7 +3059,7 @@ You can download photos from multiple devices simultaneously, or you can specify <property name="label" translatable="yes">Backup</property> </object> <packing> - <property name="position">7</property> + <property name="position">8</property> <property name="tab_fill">False</property> </packing> </child> @@ -2956,7 +3397,7 @@ You can download photos from multiple devices simultaneously, or you can specify </child> </object> <packing> - <property name="position">8</property> + <property name="position">9</property> </packing> </child> <child type="tab"> @@ -2967,7 +3408,7 @@ You can download photos from multiple devices simultaneously, or you can specify <property name="label" translatable="yes">Miscellaneous</property> </object> <packing> - <property name="position">8</property> + <property name="position">9</property> <property name="tab_fill">False</property> </packing> </child> @@ -3278,7 +3719,7 @@ You can download photos from multiple devices simultaneously, or you can specify </child> </object> <packing> - <property name="position">9</property> + <property name="position">10</property> </packing> </child> <child type="tab"> @@ -3289,7 +3730,7 @@ You can download photos from multiple devices simultaneously, or you can specify <property name="label" translatable="yes">Error Handling</property> </object> <packing> - <property name="position">9</property> + <property name="position">10</property> <property name="tab_fill">False</property> </packing> </child> diff --git a/rapid/preferencesdialog.py b/rapid/preferencesdialog.py index 7289d0e..1509630 100644 --- a/rapid/preferencesdialog.py +++ b/rapid/preferencesdialog.py @@ -18,7 +18,7 @@ ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -import datetime +import datetime, re import gtk @@ -384,9 +384,9 @@ class VideoSubfolderPrefs(PhotoSubfolderPrefs): pref_defn_L0 = DICT_VIDEO_SUBFOLDER_L0, pref_list = pref_list) -class RemoveAllJobCodeDialog(gtk.Dialog): - def __init__(self, parent_window, post_choice_callback): - gtk.Dialog.__init__(self, _('Remove all Job Codes?'), None, +class QuestionDialog(gtk.Dialog): + def __init__(self, parent_window, title, question, post_choice_callback): + gtk.Dialog.__init__(self, title, None, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, (gtk.STOCK_NO, gtk.RESPONSE_CANCEL, gtk.STOCK_YES, gtk.RESPONSE_OK)) @@ -403,7 +403,7 @@ class RemoveAllJobCodeDialog(gtk.Dialog): image.set_from_pixbuf(icon) prompt_hbox.pack_start(image, False, False, padding = 6) - prompt_label = gtk.Label(_('Should all Job Codes be removed?')) + prompt_label = gtk.Label(question) prompt_label.set_line_wrap(True) prompt_hbox.pack_start(prompt_label, False, False, padding=6) @@ -420,11 +420,32 @@ class RemoveAllJobCodeDialog(gtk.Dialog): self.connect('response', self.on_response) - + def on_response(self, device_dialog, response): user_selected = response == gtk.RESPONSE_OK self.post_choice_callback(self, user_selected) +class RemoveAllJobCodeDialog(QuestionDialog): + def __init__(self, parent_window, post_choice_callback): + QuestionDialog.__init__(self, parent_window, + _('Remove all Job Codes?'), + _('Should all Job Codes be removed?'), + 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) + +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) + class PhotoRenameTable(tpm.TablePlusMinus): def __init__(self, preferencesdialog, adjust_scroll_window): @@ -716,7 +737,6 @@ class JobCodeDialog(gtk.Dialog): (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OK, gtk.RESPONSE_OK)) - self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) self.post_job_code_entry_callback = post_job_code_entry_callback @@ -788,6 +808,54 @@ class JobCodeDialog(gtk.Dialog): logger.debug("Job Code not entered") self.post_job_code_entry_callback(self, user_chose_code, self.get_job_code()) +class IgnorePathDialog(gtk.Dialog): + """ Dialog prompting for a path to ignore when scanning devices""" + + def __init__(self, parent_window, post_entry_callback): + gtk.Dialog.__init__(self, _('Enter a Path to Ignore'), None, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OK, gtk.RESPONSE_OK)) + + self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) + self.post_entry_callback = post_entry_callback + + self.path_entry = gtk.Entry(max=0) + + self.ignored_path_hbox = gtk.HBox(homogeneous = False) + + task_label = gtk.Label(_('Specify a path that will never be scanned for photos or videos')) + task_label.set_line_wrap(True) + task_hbox = gtk.HBox() + task_hbox.pack_start(task_label, False, False, padding=6) + + label = gtk.Label(_('Path:')) + self.ignored_path_hbox.pack_start(label, False, False, padding=6) + self.ignored_path_hbox.pack_start(self.path_entry, True, True, padding=6) + + self.set_border_width(6) + self.set_has_separator(False) + + # when user hits enter, close the dialog window + self.set_default_response(gtk.RESPONSE_OK) + self.path_entry.set_activates_default(True) + + self.vbox.pack_start(task_hbox, False, False, padding = 6) + self.vbox.pack_start(self.ignored_path_hbox, False, False, padding=12) + + self.set_transient_for(parent_window) + self.show_all() + self.connect('response', self.on_ignored_path_resp) + + def on_ignored_path_resp(self, ignored_path_dialog, response): + user_chose_path = False + if response == gtk.RESPONSE_OK: + user_chose_path = True + logger.debug("Ignored Path entered") + else: + logger.debug("Ignored Path not entered") + self.post_entry_callback(self, user_chose_path, self.path_entry.get_text()) + class PreferencesDialog(): """ @@ -834,6 +902,7 @@ class PreferencesDialog(): self._setup_rename_options_tab() self._setup_job_code_tab() self._setup_device_tab() + self._setup_device_options_tab() self._setup_backup_tab() self._setup_miscellaneous_tab() self._setup_error_tab() @@ -903,8 +972,101 @@ class PreferencesDialog(): self.prefs.backup_location = widget.get_current_folder() self.update_backup_example() + def on_backup_video_folder_filechooser_button_selection_changed(self, widget): + self.prefs.backup_video_location = widget.get_current_folder() + self.update_backup_example() + def on_device_location_filechooser_button_selection_changed(self, widget): self.prefs.device_location = widget.get_current_folder() + + def on_add_ignored_path_button_clicked(self, widget): + i = IgnorePathDialog(parent_window = self.dialog, + post_entry_callback = self.add_ignored_path) + + def add_ignored_path(self, dialog, user_chose_path, path): + dialog.destroy() + if user_chose_path: + if path and path not in self.prefs.ignored_paths: + self.ignored_paths_liststore.prepend((path, )) + self.update_ignored_paths() + selection = self.ignored_paths_treeview.get_selection() + selection.unselect_all() + selection.select_path((0, )) + #scroll to the top + adjustment = self.ignored_paths_scrolledwindow.get_vadjustment() + adjustment.set_value(adjustment.lower) + + def on_ignored_paths_use_re_checkbutton_toggled(self, checkbutton): + self.prefs.use_re_ignored_paths = checkbutton.get_active() + if self.prefs.use_re_ignored_paths and not self.pref_dialog_startup: + # check for invalid regular expressions + self.update_ignored_paths() + + def on_remove_ignored_path_button_clicked(self, button): + self._remove_from_treeview(self.ignored_paths_treeview) + self.update_ignored_paths() + + def on_remove_all_ignored_paths_button_clicked(self, button): + i = RemoveAllIgnoredPathsDialog(self.dialog, self.remove_all_ignored_paths) + + def remove_all_ignored_paths(self, dialog, user_selected): + dialog.destroy() + if user_selected: + self.ignored_paths_liststore.clear() + self.update_ignored_paths() + + def on_remove_remembered_device_button_clicked(self, button): + """ + uses remembered devices treeview to delete any removed items from the + device_whitelist and device_blacklist prefs + """ + blacklist = [i for i in self.prefs.device_blacklist if i] + whitelist = [i for i in self.prefs.device_whitelist if i] + selection = self.remembered_devices_treeview.get_selection() + model, selected = selection.get_selected_rows() + iters = [model.get_iter(path) for path in selected] + # only delete if a value is selected + if iters: + no = len(iters) + path = None + for i in range(0, no): + iter = iters[i] + if i == no - 1: + path = model.get_path(iter) + v = self.remembered_devices_liststore.get_value(iter, 0) + if v in blacklist: + blacklist.remove(v) + elif v in whitelist: + whitelist.remove(v) + else: + logger.debug("Unknown remembered device %s", v) + model.remove(iter) + + # now that we removed the selection, play nice with + # the user and select the next item + selection.select_path(path) + + # if there was no selection that meant the user + # removed the last entry, so we try to select the + # last item + if not selection.path_is_selected(path): + row = path[0]-1 + # test case for empty lists + if row >= 0: + selection.select_path((row,)) + + self.prefs.device_blacklist = blacklist + self.prefs.device_whitelist = whitelist + + def on_remove_all_remembered_device_button_clicked(self, button): + r = RemoveAllRemeberedDevicesDialog(self.dialog, self.remove_all_remembered_devices) + + def remove_all_remembered_devices(self, dialog, user_selected): + dialog.destroy() + if user_selected: + self.remembered_devices_liststore.clear() + self.prefs.device_blacklist = [] + self.prefs.device_whitelist = [] def _setup_sample_names(self, use_dummy_data = False): """ @@ -1085,7 +1247,7 @@ class PreferencesDialog(): column = gtk.TreeViewColumn() rentext = gtk.CellRendererText() rentext.connect('edited', self.on_job_code_edited) - rentext .set_property('editable', True) + rentext.set_property('editable', True) column.pack_start(rentext, expand=0) column.set_attributes(rentext, text=0) @@ -1101,6 +1263,60 @@ class PreferencesDialog(): self.remove_all_job_code_button.set_image(gtk.image_new_from_stock( gtk.STOCK_CLEAR, gtk.ICON_SIZE_BUTTON)) + def _setup_device_options_tab(self): + """ + Setup ignored paths and remembered devices tab in prefs dialog + """ + + self.ignored_paths_use_re_checkbutton.set_active( + self.prefs.use_re_ignored_paths) + + self.ignored_paths_liststore = gtk.ListStore(str) + column = gtk.TreeViewColumn() + rentext = gtk.CellRendererText() + rentext.connect('edited', self.on_ignored_path_edited) + rentext.set_property('editable', True) + + column.pack_start(rentext, expand=0) + column.set_attributes(rentext, text=0) + self.ignored_paths_treeview_column = column + self.ignored_paths_treeview.append_column(column) + self.ignored_paths_treeview.props.model = self.ignored_paths_liststore + for path in self.prefs.ignored_paths: + self.ignored_paths_liststore.append((path, )) + + self.ignored_paths_treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE) + self.remove_all_ignored_paths_button.set_image(gtk.image_new_from_stock( + gtk.STOCK_CLEAR, + gtk.ICON_SIZE_BUTTON)) + + # Remembered devices are a little different in that they cannot be + # edited, and they can only added when the user is prompted by the + # program. Moreover, the list the user sees is a combination of two + # lists: device_whitelist and device_blacklist + + self.remembered_devices_liststore = gtk.ListStore(str) + column = gtk.TreeViewColumn() + rentext = gtk.CellRendererText() + rentext.set_property('editable', False) + + column.pack_start(rentext, expand=0) + column.set_attributes(rentext, text=0) + self.remembered_devices_treeview_column = column + self.remembered_devices_treeview.append_column(column) + self.remembered_devices_treeview.props.model = self.remembered_devices_liststore + for device in self.prefs.device_whitelist: + if device: + self.remembered_devices_liststore.append((device, )) + for device in self.prefs.device_blacklist: + if device: + self.remembered_devices_liststore.append((device, )) + + self.remembered_devices_treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE) + self.remove_all_remembered_device_button.set_image(gtk.image_new_from_stock( + gtk.STOCK_CLEAR, + gtk.ICON_SIZE_BUTTON)) + def _setup_device_tab(self): self.device_location_filechooser_button = gtk.FileChooserButton( @@ -1125,8 +1341,12 @@ class PreferencesDialog(): def _setup_backup_tab(self): + """ + Setup and configure backup tab + """ + #Manual backup location for photos file chooser self.backup_folder_filechooser_button = gtk.FileChooserButton( - _("Select a folder in which to backup %(file_types)s") % {'file_types':self.file_types}) + _("Select a folder in which to backup photos")) self.backup_folder_filechooser_button.set_current_folder( self.prefs.backup_location) self.backup_folder_filechooser_button.set_action( @@ -1136,6 +1356,20 @@ class PreferencesDialog(): self.backup_table.attach(self.backup_folder_filechooser_button, 3, 4, 8, 9, yoptions = gtk.SHRINK) self.backup_folder_filechooser_button.show() + + #Manual backup location for videos file chooser + self.backup_video_folder_filechooser_button = gtk.FileChooserButton( + _("Select a folder in which to backup videos")) + self.backup_video_folder_filechooser_button.set_current_folder( + self.prefs.backup_video_location) + self.backup_video_folder_filechooser_button.set_action( + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + self.backup_video_folder_filechooser_button.connect("selection-changed", + self.on_backup_video_folder_filechooser_button_selection_changed) + self.backup_table.attach(self.backup_video_folder_filechooser_button, + 3, 4, 9, 10, yoptions = gtk.SHRINK) + self.backup_video_folder_filechooser_button.show() + self.backup_identifier_entry.set_text(self.prefs.backup_identifier) self.video_backup_identifier_entry.set_text(self.prefs.video_backup_identifier) @@ -1149,6 +1383,14 @@ class PreferencesDialog(): self._backup_controls2 = [self.backup_location_label, self.backup_folder_filechooser_button, self.backup_location_explanation_label] + + if metadatavideo.DOWNLOAD_VIDEO: + self._backup_controls2 += [self.backup_video_folder_filechooser_button, + self.backup_video_location_label] + else: + self.backup_video_folder_filechooser_button.set_sensitive(False) + self.backup_video_location_label.set_sensitive(False) + self._backup_controls = self._backup_controls0 + self._backup_controls1 + \ self._backup_controls2 @@ -1400,12 +1642,14 @@ class PreferencesDialog(): adjustment = self.job_code_scrolledwindow.get_vadjustment() adjustment.set_value(adjustment.lower) - def on_remove_job_code_button_clicked(self, button): - """ remove selected job codes (can be multiple selection)""" - selection = self.job_code_treeview.get_selection() + def _remove_from_treeview(self, treeview): + """ + Removes selected items from a treeview, allowing multiple selections + """ + selection = treeview.get_selection() model, selected = selection.get_selected_rows() iters = [model.get_iter(path) for path in selected] - # only delete if a jobe code is selected + # only delete if a value is selected if iters: no = len(iters) path = None @@ -1419,7 +1663,7 @@ class PreferencesDialog(): # the user and select the next item selection.select_path(path) - # if there was no selection that meant the user + # if there was no selection that meant the user # removed the last entry, so we try to select the # last item if not selection.path_is_selected(path): @@ -1427,14 +1671,19 @@ class PreferencesDialog(): # test case for empty lists if row >= 0: selection.select_path((row,)) - + + + def on_remove_job_code_button_clicked(self, button): + """ remove selected job codes (can be multiple selection)""" + + self._remove_from_treeview(self.job_code_treeview) self.update_job_codes() self.update_photo_rename_example() self.update_video_rename_example() self.update_photo_download_folder_example() self.update_video_download_folder_example() - def on_remove_all_job_code_button_clicked(self, button): + def on_remove_all_job_code_button_clicked(self, button): j = RemoveAllJobCodeDialog(self.dialog, self.remove_all_job_code) def remove_all_job_code(self, dialog, user_selected): @@ -1456,13 +1705,51 @@ class PreferencesDialog(): self.update_photo_download_folder_example() self.update_video_download_folder_example() + def _update_prefs_list(self, liststore): + replacement_list = [] + for row in liststore: + replacement_list.append(row[0]) + return replacement_list + def update_job_codes(self): """ update preferences with list of job codes""" - job_codes = [] - for row in self.job_code_liststore: - job_codes.append(row[0]) - self.prefs.job_codes = job_codes - + self.prefs.job_codes = self._update_prefs_list(self.job_code_liststore) + + def on_ignored_path_edited(self, widget, path, new_text): + iter = self.ignored_paths_liststore.get_iter(path) + self.ignored_paths_liststore.set_value(iter, 0, new_text) + self.update_ignored_paths() + + def update_ignored_paths(self): + ignored_paths = self._update_prefs_list(self.ignored_paths_liststore) + + # remove any trailing slashes + ignored_paths = [path.rstrip('/') for path in ignored_paths if path] + # remove any blank values from ignored_paths + ignored_paths = [path for path in ignored_paths if path] + + if self.prefs.use_re_ignored_paths: + ip = [] + bad_paths = '' + for path in ignored_paths: + # check for validity + try: + re.match(path, '') + ip.append(path) + except: + logger.error("Ignoring invalid regular expression: %s", path) + bad_paths += path + '\n' + ignored_paths = ip + if bad_paths: + bad_paths = bad_paths[:-1] + if bad_paths.find('\n') >= 0: + msg = _("The following regular expressions are invalid, and will be removed unless you correct them:\n %s") % bad_paths + else: + msg = _("This regular expression is invalid, and will be removed unless you correct it:\n %s") % bad_paths + misc.run_dialog(_("Invalid regular expression"), msg, self) + + self.prefs.ignored_paths = ignored_paths + def on_auto_startup_checkbutton_toggled(self, checkbutton): self.prefs.auto_download_at_startup = checkbutton.get_active() @@ -1471,7 +1758,6 @@ 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() diff --git a/rapid/prefsrapid.py b/rapid/prefsrapid.py index f13530f..080e6f9 100644 --- a/rapid/prefsrapid.py +++ b/rapid/prefsrapid.py @@ -17,7 +17,7 @@ ### 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, os, datetime +import subprocess, os, datetime, re import prefs @@ -91,6 +91,9 @@ class RapidPreferences(prefs.Preferences): "device_autodetection_psd": prefs.Value(prefs.BOOL, False), "device_whitelist": prefs.ListValue(prefs.STRING_LIST, ['']), "device_blacklist": prefs.ListValue(prefs.STRING_LIST, ['']), + "ignored_paths": prefs.ListValue(prefs.STRING_LIST, ['.Trash', + '.thumbnails']), + "use_re_ignored_paths": prefs.Value(prefs.BOOL, False), "backup_images": prefs.Value(prefs.BOOL, False), "backup_device_autodetection": prefs.Value(prefs.BOOL, True), "backup_identifier": prefs.Value(prefs.STRING, @@ -98,6 +101,7 @@ class RapidPreferences(prefs.Preferences): "video_backup_identifier": prefs.Value(prefs.STRING, get_default_backup_video_identifier()), "backup_location": prefs.Value(prefs.STRING, os.path.expanduser('~')), + "backup_video_location": prefs.Value(prefs.STRING, os.path.expanduser('~')), "strip_characters": prefs.Value(prefs.BOOL, True), "auto_download_at_startup": prefs.Value(prefs.BOOL, False), "auto_download_upon_device_insertion": prefs.Value(prefs.BOOL, False), @@ -412,5 +416,33 @@ def format_pref_list_for_pretty_print(pref_list): s = "%s (%s)" % (s, pref_list[i+2]) v += s + "\n" return v - - + + +def check_and_compile_re(ignored_paths): + """ + configure regular expression to search for, checking to see it is valid + + returns compiled RE, or None if invalid + """ + + pattern = '' + for path in ignored_paths: + # check for validity + try: + re.match(path, '') + pattern += '.*%s$|' % path + except: + logger.error("Ignoring invalid regular expression: %s", path) + if pattern: + pattern = pattern[:-1] + + logger.debug("Ignored paths regular expression pattern: %s", pattern) + if not pattern: + logger.warning("No regular expression is specified") + return None + else: + try: + return re.compile(pattern) + except: + logger.error('This regular expression is invalid: %s', pattern) + return None diff --git a/rapid/problemnotification.py b/rapid/problemnotification.py index 6238847..95e31a4 100755 --- a/rapid/problemnotification.py +++ b/rapid/problemnotification.py @@ -99,7 +99,7 @@ problem_definitions = { BACKUP_DIRECTORY_CREATION: (BACKUP_PROBLEM, "%s", True), NO_DOWNLOAD_WAS_BACKED_UP: (BACKUP_OK, "%s", True), - SAME_FILE_DIFFERENT_EXIF: (DIFFERENT_EXIF, _("%(image1)s was taken at on %(image1_date)s at %(image1_time)s, and %(image2)s on %(image2_date)s at %(image2_time)s."), False), + SAME_FILE_DIFFERENT_EXIF: (DIFFERENT_EXIF, _("%(image1)s was taken on %(image1_date)s at %(image1_time)s, and %(image2)s on %(image2_date)s at %(image2_time)s."), False), FILE_ALREADY_DOWNLOADED: (FILE_ALREADY_DOWN_CAT, _('%(filetype)s was already downloaded'), False), } diff --git a/rapid/rapid.py b/rapid/rapid.py index ce4fa00..db5bdc6 100755 --- a/rapid/rapid.py +++ b/rapid/rapid.py @@ -68,6 +68,7 @@ import scan as scan_process import copyfiles import subfolderfile import backupfile +from backupfile import PHOTO_BACKUP, VIDEO_BACKUP, PHOTO_VIDEO_BACKUP import errorlog @@ -890,9 +891,10 @@ class ThumbnailDisplay(gtk.IconView): def generate_thumbnails(self, scan_pid): """Initiate thumbnail generation for files scanned in one process """ - rpd_files = [self.rpd_files[unique_id] for unique_id in self.process_index[scan_pid]] - thumbnail_pid = self.thumbnail_manager.add_task((scan_pid, rpd_files)) - self.generating_thumbnails[scan_pid] = thumbnail_pid + if scan_pid in self.process_index: + rpd_files = [self.rpd_files[unique_id] for unique_id in self.process_index[scan_pid]] + thumbnail_pid = self.thumbnail_manager.add_task((scan_pid, rpd_files)) + self.generating_thumbnails[scan_pid] = thumbnail_pid def _set_thumbnail(self, unique_id, icon): treerowref = self.treerow_index[unique_id] @@ -991,7 +993,7 @@ class ThumbnailDisplay(gtk.IconView): self.rapid_app.update_preview_image(unique_id, preview_image) # user can turn off option for thumbnail generation after a scan - if unique_id not in self.thumbnails: + if unique_id not in self.thumbnails and preview_small is not None: self._set_thumbnail(unique_id, preview_small.get_image()) @@ -1150,16 +1152,22 @@ class TaskManager: class ScanManager(TaskManager): - def __init__(self, results_callback, batch_size, generate_folder, + def __init__(self, results_callback, batch_size, add_device_function): TaskManager.__init__(self, results_callback, batch_size) self.add_device_function = add_device_function - self.generate_folder = generate_folder - def _initiate_task(self, device, task_results_conn, task_process_conn, + def _initiate_task(self, task, task_results_conn, task_process_conn, terminate_queue, run_event): - - scan = scan_process.Scan(device.get_path(), self.batch_size, self.generate_folder, + + device = task[0] + ignored_paths = task[1] + use_re_ignored_paths = task[2] + + scan = scan_process.Scan(device.get_path(), + ignored_paths, + use_re_ignored_paths, + self.batch_size, task_process_conn, terminate_queue, run_event) scan.start() self._processes.append((scan, terminate_queue, run_event)) @@ -1223,6 +1231,7 @@ class BackupFilesManager(TaskManager): terminate_queue, run_event): path = task[0] name = task[1] + backup_type = task[2] backup_files = backupfile.BackupFiles(path, name, self.batch_size, task_process_conn, terminate_queue, run_event) @@ -1230,22 +1239,36 @@ class BackupFilesManager(TaskManager): self._processes.append((backup_files, terminate_queue, run_event, task_results_conn)) - self.backup_devices_by_path[path] = (task_results_conn, backup_files.pid) + self.backup_devices_by_path[path] = (task_results_conn, backup_files.pid, + backup_type) return backup_files.pid def backup_file(self, move_succeeded, rpd_file, path_suffix, backup_duplicate_overwrite): + + if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: + logger.debug("Backing up photo %s", rpd_file.download_name) + else: + logger.debug("Backing up video %s", rpd_file.download_name) + for path in self.backup_devices_by_path: - task_results_conn = self.backup_devices_by_path[path][0] - task_results_conn.send((move_succeeded, rpd_file, path_suffix, + backup_type = self.backup_devices_by_path[path][2] + if ((backup_type == PHOTO_VIDEO_BACKUP) or + (rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO and backup_type == PHOTO_BACKUP) or + (rpd_file.file_type == rpdfile.FILE_TYPE_VIDEO and backup_type == VIDEO_BACKUP)): + logger.debug("Backing up to %s", path) + task_results_conn = self.backup_devices_by_path[path][0] + task_results_conn.send((move_succeeded, rpd_file, path_suffix, backup_duplicate_overwrite)) + else: + logger.debug("Not backing up to %s", path) - def add_device(self, path, name): + def add_device(self, path, name, backup_type): """ Convenience function to setup adding a backup device """ - return self.add_task((path, name)) + return self.add_task((path, name, backup_type)) def remove_device(self, path): pid = self.backup_devices_by_path[path][1] @@ -1289,9 +1312,10 @@ class SubfolderFileManager(SingleInstanceTaskManager): """ Manages the daemon process that renames files and creates subfolders """ - def __init__(self, results_callback, sequence_values): + def __init__(self, results_callback, sequence_values, focal_length): SingleInstanceTaskManager.__init__(self, results_callback) - self._subfolder_file = subfolderfile.SubfolderFile(self.task_process_conn, sequence_values) + self._subfolder_file = subfolderfile.SubfolderFile(self.task_process_conn, + sequence_values, focal_length) self._subfolder_file.start() logger.debug("SubfolderFile PID: %s", self._subfolder_file.pid) @@ -1427,13 +1451,15 @@ class RapidApp(dbus.service.Object): processes. """ - def __init__(self, bus, path, name, taskserver=None): + def __init__(self, bus, path, name, taskserver=None, focal_length=None): dbus.service.Object.__init__ (self, bus, path, name) self.running = False self.taskserver = taskserver + self.focal_length = focal_length + # Setup program preferences, and set callback for when they change self._init_prefs() @@ -1634,12 +1660,21 @@ class RapidApp(dbus.service.Object): def _backup_device_name(self, path): - if self.backup_devices[path] is None: + if self.backup_devices[path][0] is None: name = path else: - name = self.backup_devices[path].get_name() + name = self.backup_devices[path][0].get_name() return name + def start_device_scan(self, device): + """ + Commences the scanning of a device using the preference values for + any paths to ignore while scanning + """ + return self.scan_manager.add_task([device, + self.prefs.ignored_paths, + self.prefs.use_re_ignored_paths]) + def setup_devices(self, on_startup, on_preference_change, block_auto_start): """ @@ -1681,13 +1716,16 @@ class RapidApp(dbus.service.Object): logger.info("%s ignored", mount.get_name()) else: logger.info("Detected %s", mount.get_name()) - is_backup_mount = self.check_if_backup_mount(path) + is_backup_mount, backup_file_type = self.check_if_backup_mount(path) if is_backup_mount: - self.backup_devices[path] = mount + self.backup_devices[path] = (mount, backup_file_type) elif (self.prefs.device_autodetection and (dv.is_DCIM_device(path) or self.search_for_PSD())): + logger.debug("Appending %s", mount.get_name()) mounts.append((path, mount)) + else: + logger.debug("Ignoring %s", mount.get_name()) if not self.prefs.device_autodetection: @@ -1702,14 +1740,8 @@ class RapidApp(dbus.service.Object): if self.prefs.backup_images: if not self.prefs.backup_device_autodetection: - # user manually specified backup location - # will backup to this path, but don't need any volume info - # associated with it - self.backup_devices[self.prefs.backup_location] = None - - for path in self.backup_devices: - name = self._backup_device_name(path) - self.backup_manager.add_device(path, name) + self._setup_manual_backup() + self._add_backup_devices() self.update_no_backup_devices() @@ -1733,12 +1765,44 @@ class RapidApp(dbus.service.Object): # prompt user to see if device should be used or not self.get_use_device(device) else: - scan_pid = self.scan_manager.add_task(device) + scan_pid = self.start_device_scan(device) if mount is not None: self.mounts_by_path[path] = scan_pid if not mounts: self.set_download_action_sensitivity() + def _setup_manual_backup(self): + """ + Setup backup devices that the user has manually specified. + Depending on the folder the user has chosen, the paths for photo and + video backup will either be the same or they will differ. + """ + # user manually specified backup locations + # will backup to these paths, but don't need any volume info + # associated with them + self.backup_devices[self.prefs.backup_location] = (None, PHOTO_BACKUP) + if DOWNLOAD_VIDEO: + if self.prefs.backup_location <> self.prefs.backup_video_location: + self.backup_devices[self.prefs.backup_video_location] = (None, VIDEO_BACKUP) + logger.info("Backing up photos to %s", self.prefs.backup_location) + logger.info("Backing up videos to %s", self.prefs.backup_video_location) + else: + # videos and photos are being backed up to the same location + self.backup_devices[self.prefs.backup_location] = (None, PHOTO_VIDEO_BACKUP) + logger.info("Backing up photos and videos to %s", self.prefs.backup_location) + else: + logger.info("Backing up photos to %s", self.prefs.backup_location) + + def _add_backup_devices(self): + """ + Add each backup devices / path to backup manager + """ + for path in self.backup_devices: + name = self._backup_device_name(path) + backup_type = self.backup_devices[path][1] + self.backup_manager.add_device(path, name, backup_type) + + def get_use_device(self, device): """ Prompt user whether or not to download from this device """ @@ -1759,7 +1823,7 @@ class RapidApp(dbus.service.Object): self.prefs.device_whitelist = self.prefs.device_whitelist + [path] else: self.prefs.device_whitelist = [path] - scan_pid = self.scan_manager.add_task(device) + scan_pid = self.start_device_scan(device) self.mounts_by_path[path] = scan_pid elif permanent_choice and path not in self.prefs.device_blacklist: @@ -1776,26 +1840,67 @@ class RapidApp(dbus.service.Object): """ return self.prefs.device_autodetection_psd and self.prefs.device_autodetection - def check_if_backup_mount(self, path): + def check_if_backup_mount(self, path): """ - Checks to see if backups are enabled and path represents a valid backup location + Checks to see if backups are enabled and path represents a valid backup + location. It must be writeable. Checks against user preferences. + + Returns a tuple: + (True, <backup-type> (one of PHOTO_VIDEO_BACKUP, PHOTO_BACKUP, or VIDEO_BACKUP)) or + (False, None) """ - identifiers = [self.prefs.backup_identifier] - if DOWNLOAD_VIDEO: - identifiers.append(self.prefs.video_backup_identifier) if self.prefs.backup_images: if self.prefs.backup_device_autodetection: - if dv.is_backup_media(path, identifiers): - return True + # Determine if the auto-detected backup device is + # to be used to backup only photos, or videos, or both. + # Use the presence of a corresponding directory to + # determine this. + # The directory must be writable. + photo_path = os.path.join(path, self.prefs.backup_identifier) + p_backup = os.path.isdir(photo_path) and os.access(photo_path, os.W_OK) + if DOWNLOAD_VIDEO: + video_path = os.path.join(path, self.prefs.video_backup_identifier) + v_backup = os.path.isdir(video_path) and os.access(video_path, os.W_OK) + else: + v_backup = False + if p_backup and v_backup: + logger.info("Photos and videos will be backed up to %s", path) + return (True, PHOTO_VIDEO_BACKUP) + elif p_backup: + logger.info("Photos will be backed up to %s", path) + return (True, PHOTO_BACKUP) + elif v_backup: + logger.info("Videos will be backed up to %s", path) + return (True, VIDEO_BACKUP) elif path == self.prefs.backup_location: + # user manually specified the path + if os.access(self.prefs.backup_location, os.W_OK): + return (True, PHOTO_BACKUP) + elif path == self.prefs.backup_video_location: # user manually specified the path - return True - return False + if os.access(self.prefs.backup_video_location, os.W_OK): + return (True, VIDEO_BACKUP) + return (False, None) def update_no_backup_devices(self): - self.download_tracker.set_no_backup_devices(len(self.backup_devices)) + self.no_photo_backup_devices = 0 + self.no_video_backup_devices = 0 + for path, value in self.backup_devices.iteritems(): + backup_type = value[1] + if backup_type == PHOTO_BACKUP: + self.no_photo_backup_devices += 1 + elif backup_type == VIDEO_BACKUP: + self.no_video_backup_devices += 1 + else: + #both videos and photos are backed up to this device / path + self.no_photo_backup_devices += 1 + self.no_video_backup_devices += 1 + logger.info("# photo backup devices: %s; # video backup devices: %s", + self.no_photo_backup_devices, self.no_video_backup_devices) + self.download_tracker.set_no_backup_devices(self.no_photo_backup_devices, + self.no_video_backup_devices) def refresh_backup_media(self): """ @@ -1811,22 +1916,19 @@ class RapidApp(dbus.service.Object): self.backup_devices = {} if self.prefs.backup_images: if not self.prefs.backup_device_autodetection: - # user manually specified backup location - # will backup to this path, but don't need any volume info associated with it - self.backup_devices[self.prefs.backup_location] = None + self._setup_manual_backup() else: for mount in self.vmonitor.get_mounts(): if not mount.is_shadowed(): path = mount.get_root().get_path() if path: - if self.check_if_backup_mount(path): + is_backup_mount, backup_file_type = self.check_if_backup_mount(path) + if is_backup_mount: # is a backup volume if path not in self.backup_devices: - self.backup_devices[path] = mount + self.backup_devices[path] = (mount, backup_file_type) - for path in self.backup_devices: - name = self._backup_device_name(path) - self.backup_manager.add_device(path, name) + self._add_backup_devices() self.update_no_backup_devices() self.display_free_space() @@ -1859,13 +1961,13 @@ class RapidApp(dbus.service.Object): logger.info("Device %(device)s (%(path)s) ignored" % { 'device': mount.get_name(), 'path': path}) else: - is_backup_mount = self.check_if_backup_mount(path) + is_backup_mount, backup_file_type = self.check_if_backup_mount(path) if is_backup_mount: if path not in self.backup_devices: self.backup_devices[path] = mount name = self._backup_device_name(path) - self.backup_manager.add_device(path, name) + self.backup_manager.add_device(path, name, backup_file_type) self.update_no_backup_devices() self.display_free_space() @@ -1878,7 +1980,7 @@ class RapidApp(dbus.service.Object): # prompt user if device should be used or not self.get_use_device(device) else: - scan_pid = self.scan_manager.add_task(device) + scan_pid = self.start_device_scan(device) self.mounts_by_path[path] = scan_pid def on_mount_removed(self, vmonitor, mount): @@ -2137,25 +2239,40 @@ class RapidApp(dbus.service.Object): """ Initiate downloading and renaming of files """ - # Check which file types will be downloaded for this particular process - if self.files_of_type_present(files, rpdfile.FILE_TYPE_PHOTO): + no_photos_to_download = self.files_of_type_present(files, + rpdfile.FILE_TYPE_PHOTO, + return_file_count=True) + if no_photos_to_download: photo_download_folder = self.prefs.download_folder else: photo_download_folder = None - if self.files_of_type_present(files, rpdfile.FILE_TYPE_VIDEO): - video_download_folder = self.prefs.video_download_folder + if DOWNLOAD_VIDEO: + no_videos_to_download = self.files_of_type_present(files, + rpdfile.FILE_TYPE_VIDEO, + return_file_count=True) + if no_videos_to_download: + video_download_folder = self.prefs.video_download_folder + else: + video_download_folder = None else: video_download_folder = None + no_videos_to_download = 0 - download_size = self.size_files_to_be_downloaded(files) + photo_download_size, video_download_size = self.size_files_to_be_downloaded(files) self.download_tracker.init_stats(scan_pid=scan_pid, - bytes=download_size, - no_files=len(files)) + photo_size_in_bytes=photo_download_size, + video_size_in_bytes=video_download_size, + no_photos_to_download=no_photos_to_download, + no_videos_to_download=no_videos_to_download) + + + download_size = photo_download_size + video_download_size if self.prefs.backup_images: - download_size = download_size * (len(self.backup_devices) + 1) + download_size = download_size + ((self.no_photo_backup_devices * photo_download_size) + + (self.no_video_backup_devices * video_download_size)) self.time_remaining.set(scan_pid, download_size) self.time_check.set_download_mark() @@ -2255,10 +2372,6 @@ class RapidApp(dbus.service.Object): if rpd_file.status == config.STATUS_DOWNLOADED_WITH_WARNING: self.log_error(config.WARNING, rpd_file.error_title, rpd_file.error_msg, rpd_file.error_extra_detail) - self.error_title = '' - self.error_msg = '' - self.error_extra_detail = '' - if self.prefs.backup_images and len(self.backup_devices): if self.prefs.backup_device_autodetection: @@ -2275,8 +2388,20 @@ class RapidApp(dbus.service.Object): else: self.file_download_finished(move_succeeded, rpd_file) - + + def multiple_backup_devices(self, file_type): + """Returns true if more than one backup device is being used for that + file type + """ + return ((file_type == rpdfile.FILE_TYPE_PHOTO and + self.no_photo_backup_devices > 1) or + (file_type == rpdfile.FILE_TYPE_VIDEO and + self.no_video_backup_devices > 1)) + def backup_results(self, source, condition): + """ + Handle results sent from backup processes + """ connection = self.backup_manager.get_pipe(source) conn_type, msg_data = connection.recv() if conn_type == rpdmp.CONN_PARTIAL: @@ -2294,8 +2419,20 @@ class RapidApp(dbus.service.Object): elif msg_type == rpdmp.MSG_FILE: backup_succeeded, rpd_file = data + + # Only show an error message if there is more than one device + # backing up files of this type - if that is the case, + # do not want to reply on showing an error message in the + # function file_download_finished, as it is only called once, + # when all files have been backed up + if not backup_succeeded and self.multiple_backup_devices(rpd_file.file_type): + self.log_error(config.SERIOUS_ERROR, + rpd_file.error_title, + rpd_file.error_msg, rpd_file.error_extra_detail) + self.download_tracker.file_backed_up(rpd_file.unique_id) - if self.download_tracker.all_files_backed_up(rpd_file.unique_id): + if self.download_tracker.all_files_backed_up(rpd_file.unique_id, + rpd_file.file_type): self.file_download_finished(backup_succeeded, rpd_file) return True else: @@ -2303,10 +2440,13 @@ class RapidApp(dbus.service.Object): def file_download_finished(self, succeeded, rpd_file): + """ + Called when a file has been downloaded i.e. copied, renamed, and backed up + """ scan_pid = rpd_file.scan_pid unique_id = rpd_file.unique_id # Update error log window if neccessary - if not succeeded: + if not succeeded and not self.multiple_backup_devices(rpd_file.file_type): self.log_error(config.SERIOUS_ERROR, rpd_file.error_title, rpd_file.error_msg, rpd_file.error_extra_detail) elif self.prefs.auto_delete: @@ -2319,7 +2459,7 @@ class RapidApp(dbus.service.Object): rpd_file.file_type, rpd_file.status) - completed, files_remaining = self._update_file_download_device_progress(scan_pid, unique_id) + completed, files_remaining = self._update_file_download_device_progress(scan_pid, unique_id, rpd_file.file_type) if self.download_is_occurring(): self.update_time_remaining() @@ -2517,7 +2657,7 @@ class RapidApp(dbus.service.Object): self.display_summary_notification = False # don't show it again unless needed - def _update_file_download_device_progress(self, scan_pid, unique_id): + def _update_file_download_device_progress(self, scan_pid, unique_id, file_type): """ Increments the progress bar for an individual device @@ -2531,7 +2671,7 @@ class RapidApp(dbus.service.Object): file_types = self.download_tracker.get_file_types_present(scan_pid) completed = files_downloaded == files_to_download if completed and (self.prefs.backup_images and len(self.backup_devices)): - completed = self.download_tracker.all_files_backed_up(unique_id) + completed = self.download_tracker.all_files_backed_up(unique_id, file_type) if completed: files_remaining = self.thumbnails.get_no_files_remaining(scan_pid) @@ -2628,6 +2768,11 @@ class RapidApp(dbus.service.Object): # related values in the preferences dialog window self.refresh_downloads_today = False + # these values are used to track the number of backup devices / + # locations for each file type + self.no_photo_backup_devices = 0 + self.no_video_backup_devices = 0 + self.downloads_today_tracker = self.prefs.get_downloads_today_tracker() downloads_today = self.downloads_today_tracker.get_and_maybe_reset_downloads_today() @@ -2648,6 +2793,7 @@ class RapidApp(dbus.service.Object): self.uses_session_sequece_no_value = Value(c_bool, self.prefs.any_pref_uses_session_sequece_no()) self.uses_sequence_letter_value = Value(c_bool, self.prefs.any_pref_uses_sequence_letter_value()) + self.check_prefs_upgrade(__version__) self.prefs.program_version = __version__ def _check_for_sequence_value_use(self): @@ -2655,6 +2801,33 @@ class RapidApp(dbus.service.Object): self.uses_session_sequece_no_value.value = self.prefs.any_pref_uses_session_sequece_no() self.uses_sequence_letter_value.value = self.prefs.any_pref_uses_sequence_letter_value() + def check_prefs_upgrade(self, running_version): + """ + Checks if the running version of the program is different from the + version recorded in the preferences. + + If the version is different, the preferences are checked to see + whether they should be upgraded or not. + """ + previous_version = self.prefs.program_version + if len(previous_version) > 0: + # the program has been run previously for this user + + pv = utilities.pythonify_version(previous_version) + rv = utilities.pythonify_version(running_version) + + if pv <> rv: + # 0.4.1 and below had only one manual backup location + # 0.4.2 introduced a distinct video back up location that can be manually set + # Therefore must duplicate the previous photo & video manual backup location into the + # new video field, unless it has already been changed already. + + if pv < utilities.pythonify_version('0.4.2'): + if self.prefs.backup_video_location == os.path.expanduser('~'): + self.prefs.backup_video_location = self.prefs.backup_location + logger.info("Migrated manual backup location preference to videos: %s", + self.prefs.backup_video_location) + def on_preference_changed(self, key, value): """ Called when user changes the program's preferences @@ -2663,12 +2836,16 @@ class RapidApp(dbus.service.Object): if key == 'show_log_dialog': self.menu_log_window.set_active(value) - elif key in ['device_autodetection', 'device_autodetection_psd', 'device_location']: + elif key in ['device_autodetection', 'device_autodetection_psd', + 'device_location', 'ignored_paths', + 'use_re_ignored_paths', 'device_blacklist']: self.rerun_setup_available_image_and_video_media = True if not self.preferences_dialog_displayed: self.post_preference_change() - elif key in ['backup_images', 'backup_device_autodetection', 'backup_location', 'backup_identifier', 'video_backup_identifier']: + elif key in ['backup_images', 'backup_device_autodetection', + 'backup_location', 'backup_video_location', + 'backup_identifier', 'video_backup_identifier']: self.rerun_setup_available_backup_media = True if not self.preferences_dialog_displayed: self.post_preference_change() @@ -2931,7 +3108,7 @@ class RapidApp(dbus.service.Object): elif i == (v - 1) : prefix = " " + _("and") + " " i += 1 - message = "%s%s'%s'" % (message, prefix, self.backup_devices[b].get_name()) + message = "%s%s'%s'" % (message, prefix, self.backup_devices[b][0].get_name()) if v > 1: message = _("Using backup devices") + " %s" % message @@ -3002,8 +3179,18 @@ class RapidApp(dbus.service.Object): if self.prefs.backup_images: if not self.prefs.backup_device_autodetection: - # user manually specified backup location - msg2 = _('Backing up to %(path)s') % {'path':self.prefs.backup_location} + if self.prefs.backup_location == self.prefs.backup_video_location: + if DOWNLOAD_VIDEO: + # user manually specified the same location for photos and video backups + msg2 = _('Backing up photos and videos to %(path)s') % {'path':self.prefs.backup_location} + else: + # user manually specified backup location + msg2 = _('Backing up to %(path)s') % {'path':self.prefs.backup_location} + else: + # user manually specified different locations for photo and video backups + msg2 = _('Backing up photos to %(path)s and videos to %(path2)s') % { + 'path':self.prefs.backup_location, + 'path2': self.prefs.backup_video_location} else: msg2 = self.display_backup_mounts() @@ -3047,25 +3234,39 @@ class RapidApp(dbus.service.Object): # Utility functions # # # - def files_of_type_present(self, files, file_type): + def files_of_type_present(self, files, file_type, return_file_count=False): """ Returns true if there is at least one instance of the file_type in the list of files to be copied + + If return_file_count is True, then the number of files of that type + will be counted and returned instead of True or False """ + i = 0 for rpd_file in files: if rpd_file.file_type == file_type: - return True - return False - + if return_file_count: + i += 1 + else: + return True + if not return_file_count: + return False + else: + return i + def size_files_to_be_downloaded(self, files): """ - Returns the total size of the files to be downloaded in bytes + Returns the total sizes of the photos and videos to be downloaded in bytes """ - size = 0 - for i in range(len(files)): - size += files[i].size + photo_size = 0 + video_size = 0 + for rpd_file in files: + if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: + photo_size += rpd_file.size + else: + video_size += rpd_file.size - return size + return (photo_size, video_size) def check_download_folder_validity(self, files_by_scan_pid): """ @@ -3223,12 +3424,12 @@ class RapidApp(dbus.service.Object): self.subfolder_file_manager = SubfolderFileManager( self.subfolder_file_results, - sequence_values) + sequence_values, + self.focal_length) - self.generate_folder = False self.scan_manager = ScanManager(self.scan_results, self.batch_size, - self.generate_folder, self.device_collection.add_device) + self.device_collection.add_device) self.copy_files_manager = CopyFilesManager(self.copy_files_results, self.batch_size_MB) self.backup_manager = BackupFilesManager(self.backup_results, @@ -3310,6 +3511,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("--reset-settings", action="store_true", dest="reset", help=_("reset all program settings and preferences and exit")) (options, args) = parser.parse_args() @@ -3334,14 +3536,23 @@ def start(): sys.exit(0) if options.reset: - prefs = RapidPreferences() + prefs = prefsrapid.RapidPreferences() prefs.reset() print _("All settings and preferences have been reset") sys.exit(0) + + if options.focal_length: + focal_length = options.focal_length + else: + focal_length = None 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 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: @@ -3350,7 +3561,7 @@ def start(): bus = dbus.SessionBus () request = bus.request_name (config.DBUS_NAME, dbus.bus.NAME_FLAG_DO_NOT_QUEUE) if request != dbus.bus.REQUEST_NAME_REPLY_EXISTS: - app = RapidApp(bus, '/', config.DBUS_NAME) + app = RapidApp(bus, '/', config.DBUS_NAME, focal_length=focal_length) else: # this application is already running print "Rapid Photo Downloader is already running" diff --git a/rapid/scan.py b/rapid/scan.py index 5092f0a..0d7182c 100755 --- a/rapid/scan.py +++ b/rapid/scan.py @@ -19,6 +19,7 @@ import os import multiprocessing +import re import gio import gtk @@ -27,6 +28,7 @@ import pyexiv2 import rpdmultiprocessing as rpdmp import rpdfile +import prefsrapid import logging @@ -48,13 +50,20 @@ class Scan(multiprocessing.Process): files in bytes. """ - def __init__(self, path, batch_size, generate_folder, results_pipe, + def __init__(self, path, ignored_paths, use_re_ignored_paths, + batch_size, results_pipe, terminate_queue, run_event): """Setup values needed to conduct the scan. 'path' is a string of the path to be scanned, which is passed to gio. + 'ignored_paths' is a list of paths that should not be scanned. Any path + ending with one of the values will be ignored. + + 'use_re_ignored_paths': if true, pytho regular expressions will be used + to determine which paths to ignore + 'batch_size' is the number of files that should be sent back to the calling function at one time. @@ -69,16 +78,16 @@ class Scan(multiprocessing.Process): multiprocessing.Process.__init__(self) self.path = path + self.ignored_paths = ignored_paths + self.use_re_ignored_paths = use_re_ignored_paths self.results_pipe = results_pipe self.terminate_queue = terminate_queue self.run_event = run_event self.batch_size = batch_size - self.generate_folder = generate_folder self.counter = 0 self.files = [] self.file_type_counter = rpdfile.FileTypeCounter() - - + def _gio_scan(self, path, file_size_sum): """recursive function to scan a directory and its subdirectories for photos and possibly videos""" @@ -104,7 +113,8 @@ class Scan(multiprocessing.Process): file_type = child.get_file_type() name = child.get_name() if file_type == gio.FILE_TYPE_DIRECTORY: - file_size_sum = self._gio_scan(path.get_child(name), + if not self.ignore_this_path(name): + file_size_sum = self._gio_scan(path.get_child(name), file_size_sum) if file_size_sum is None: return None @@ -150,9 +160,16 @@ class Scan(multiprocessing.Process): def run(self): """start the actual scan.""" + + if self.use_re_ignored_paths and len(self.ignored_paths): + self.re_pattern = prefsrapid.check_and_compile_re(self.ignored_paths) + source = gio.File(self.path) try: - size = self._gio_scan(source, 0) + if not self.ignore_this_path(self.path): + size = self._gio_scan(source, 0) + else: + size = None except gio.Error, inst: logger.error("Error while scanning %s: %s", self.path, inst) size = None @@ -164,3 +181,21 @@ class Scan(multiprocessing.Process): self.results_pipe.send((rpdmp.CONN_COMPLETE, (size, self.file_type_counter, self.pid))) self.results_pipe.close() + + def ignore_this_path(self, path): + """ + determines if the path should be ignored according to the preferences + chosen by the user + """ + + if len(self.ignored_paths): + if self.use_re_ignored_paths and self.re_pattern: + # regular expressions are being used + if self.re_pattern.match(path): + return True + else: + # regular expressions are not being used + if path.endswith(tuple(self.ignored_paths)): + return True + + return False diff --git a/rapid/subfolderfile.py b/rapid/subfolderfile.py index 35bd74f..a80bc84 100644 --- a/rapid/subfolderfile.py +++ b/rapid/subfolderfile.py @@ -136,7 +136,7 @@ def generate_name(rpd_file): class SubfolderFile(multiprocessing.Process): - def __init__(self, results_pipe, sequence_values): + def __init__(self, results_pipe, sequence_values, focal_length): multiprocessing.Process.__init__(self) self.daemon = True self.results_pipe = results_pipe @@ -150,6 +150,8 @@ 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): @@ -308,15 +310,16 @@ class SubfolderFile(multiprocessing.Process): else: # Generate subfolder name and new file name generation_succeeded = True - experimental = False - if experimental and rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: + + # 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("Samyang lens - adjusting focal length and aperture... ") + logger.info("Adjusting focal length and aperture for %s", rpd_file.full_file_name) #~ try: - rpd_file.metadata["Exif.Photo.FocalLength"] = fractions.Fraction(14,1) + 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") diff --git a/rapid/thumbnail.py b/rapid/thumbnail.py index 7a41e54..c987018 100644 --- a/rapid/thumbnail.py +++ b/rapid/thumbnail.py @@ -319,7 +319,7 @@ class GetPreviewImage(multiprocessing.Process): def get_stock_image(self, file_type): """ - Get stock image for file type scaled to the current size of the + Get stock image for file type scaled to the current size of the screen """ if file_type == rpdfile.FILE_TYPE_PHOTO: if self.stock_photo_thumbnail_image is None: @@ -337,7 +337,6 @@ class GetPreviewImage(multiprocessing.Process): 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)) - class GenerateThumbnails(multiprocessing.Process): |