diff options
Diffstat (limited to 'rapid')
-rw-r--r-- | rapid/ChangeLog | 37 | ||||
-rw-r--r-- | rapid/backupfile.py | 111 | ||||
-rw-r--r-- | rapid/config.py | 6 | ||||
-rw-r--r-- | rapid/generatename.py | 156 | ||||
-rw-r--r-- | rapid/glade3/about.ui | 3 | ||||
-rw-r--r-- | rapid/glade3/xmp.ui | 1376 | ||||
-rwxr-xr-x | rapid/rapid.py | 1771 | ||||
-rw-r--r-- | rapid/rpdfile.py | 3 | ||||
-rw-r--r-- | rapid/subfolderfile.py | 408 |
9 files changed, 2723 insertions, 1148 deletions
diff --git a/rapid/ChangeLog b/rapid/ChangeLog index 6a28146..cc27252 100644 --- a/rapid/ChangeLog +++ b/rapid/ChangeLog @@ -1,3 +1,38 @@ +Version 0.4.6 +------------- + +2013-01-22 + +Fixed bug #1083756: Application shows duplicate sources. +Fixed bug #1093330: Photo rename ignores SubSeconds when 00. + +Added extra debugging output to help trace program execution progress. + +Updated German and Spanish translations. + +Version 0.4.6 Beta 1 +------------- + +2012-11-26 + +Fixed bug #1023586: Added RAW file support for Nikon NRW files. Rapid Photo +Downloader uses the exiv2 program to read a photo's metadata. Although the NRW +format is not officially supported by exiv2, it appears to work. If you have +NRW files and Rapid Photo Downloader crashes while reading this files, please +file a bug report. + +Preliminary and tentative fix for bug #1025908: Application freezes under +Ubuntu 12.10. This fix should not be considered final, and needs further +testing. + +Added Arabic translation. Updated Czech, Danish, French, Italian, Norwegian, +Russian, Serbian, Spanish and Swedish translations. + +Fixed missing dependencies on python-dbus and exiv2 in Debian/control file. + +Added extra debugging output to help trace program execution progress. + + Version 0.4.5 ------------- @@ -113,6 +148,8 @@ confirm if this is really what they want to do. Fixed bug #792228: clear all thumbnails when refresh command issued. +Fixed bug #890949: Panasonic MOD format and duplicate filename issue + Fixed a bug where the device progress bar would occasionally disappear when the download device was changed. diff --git a/rapid/backupfile.py b/rapid/backupfile.py index 6581e6d..310e665 100644 --- a/rapid/backupfile.py +++ b/rapid/backupfile.py @@ -23,6 +23,7 @@ import tempfile import os import gio +import shutil import logging logger = multiprocessing.get_logger() @@ -41,7 +42,7 @@ from gettext import gettext as _ class BackupFiles(multiprocessing.Process): def __init__(self, path, name, - batch_size_MB, results_pipe, terminate_queue, + batch_size_MB, results_pipe, terminate_queue, run_event): multiprocessing.Process.__init__(self) self.results_pipe = results_pipe @@ -50,7 +51,11 @@ class BackupFiles(multiprocessing.Process): self.path = path self.mount_name = name self.run_event = run_event - + + # As of Ubuntu 12.10 / Fedora 18, the file move/rename command is running agonisingly slowly + # A hackish workaround is to replace it with the standard python function + self.use_gnome_file_operations = True + def check_termination_request(self): """ Check to see this process has not been requested to immediately terminate @@ -61,8 +66,8 @@ class BackupFiles(multiprocessing.Process): logger.info("Terminating file backup") return True return False - - + + def update_progress(self, amount_downloaded, total): # first check if process is being terminated self.amount_downloaded = amount_downloaded @@ -74,22 +79,22 @@ class BackupFiles(multiprocessing.Process): chunk_downloaded = amount_downloaded - self.bytes_downloaded if (chunk_downloaded > self.batch_size_bytes) or (amount_downloaded == total): self.bytes_downloaded = amount_downloaded - + if amount_downloaded == total: # this function is called a couple of times when total is reached self.total_reached = True - + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_BYTES, (self.scan_pid, self.pid, self.total_downloaded + amount_downloaded, chunk_downloaded)))) if amount_downloaded == total: self.bytes_downloaded = 0 - + def progress_callback(self, amount_downloaded, total): self.update_progress(amount_downloaded, total) - + def progress_callback_no_update(self, amount_downloaded, total): """called when copying very small files""" pass - + def backup_additional_file(self, dest_dir, full_file_name): """Backs up small files like XMP or THM files""" source = gio.File(full_file_name) @@ -99,55 +104,57 @@ class BackupFiles(multiprocessing.Process): try: source.copy(dest, self.progress_callback_no_update, cancellable=None) except gio.Error, inst: - logger.error("Failed to backup file %s", full_file_name) - + logger.error("Failed to backup file %s", full_file_name) + def run(self): - + self.cancel_copy = gio.Cancellable() self.bytes_downloaded = 0 self.total_downloaded = 0 - + while True: - + self.amount_downloaded = 0 - move_succeeded, rpd_file, path_suffix, backup_duplicate_overwrite = self.results_pipe.recv() + move_succeeded, rpd_file, path_suffix, backup_duplicate_overwrite, download_count = self.results_pipe.recv() if rpd_file is None: # this is a termination signal return None # pause if instructed by the caller self.run_event.wait() - + if self.check_termination_request(): return None - + backup_succeeded = False self.scan_pid = rpd_file.scan_pid - + if move_succeeded: self.total_reached = False - + source = gio.File(path=rpd_file.download_full_file_name) - + if path_suffix is None: dest_base_dir = self.path else: dest_base_dir = os.path.join(self.path, path_suffix) - - + + dest_dir = os.path.join(dest_base_dir, rpd_file.download_subfolder) backup_full_file_name = os.path.join( - dest_dir, - rpd_file.download_name) - + dest_dir, + rpd_file.download_name) + subfolder = gio.File(path=dest_dir) if not subfolder.query_exists(cancellable=None): # create the subfolders on the backup path try: + logger.debug("Creating subfolder %s on backup device %s...", dest_dir, self.mount_name) subfolder.make_directory_with_parents(cancellable=gio.Cancellable()) + logger.debug("...backup subfolder created") except gio.Error, inst: # There is a tiny chance directory may have been created by # another process between the time it takes to query and - # the time it takes to create a new directory. + # the time it takes to create a new directory. # Ignore such errors. if inst.code <> gio.ERROR_EXISTS: logger.error("Failed to create backup subfolder: %s", dest_dir) @@ -159,7 +166,7 @@ class BackupFiles(multiprocessing.Process): _("Destination directory could not be created: %(directory)s\n") % \ {'directory': subfolder, } + \ _("Source: %(source)s\nDestination: %(destination)s") % \ - {'source': rpd_file.download_full_file_name, + {'source': rpd_file.download_full_file_name, 'destination': backup_full_file_name} + "\n" + \ _("Error: %(inst)s") % {'inst': inst} @@ -168,21 +175,33 @@ class BackupFiles(multiprocessing.Process): flags = gio.FILE_COPY_OVERWRITE else: flags = gio.FILE_COPY_NONE - - try: - source.copy(dest, self.progress_callback, flags, - cancellable=self.cancel_copy) - backup_succeeded = True - except gio.Error, inst: - fileNotBackedUpMessageDisplayed = True - rpd_file.add_problem(None, pn.BACKUP_ERROR, self.mount_name) - rpd_file.add_extra_detail('%s%s' % (pn.BACKUP_ERROR, self.mount_name), inst) - rpd_file.error_title = _('Backing up error') - rpd_file.error_msg = \ - _("Source: %(source)s\nDestination: %(destination)s") % \ - {'source': rpd_file.download_full_file_name, 'destination': backup_full_file_name} + "\n" + \ - _("Error: %(inst)s") % {'inst': inst} - logger.error("%s:\n%s", rpd_file.error_title, rpd_file.error_msg) + + if self.use_gnome_file_operations: + try: + logger.debug("Backing up file %s on device %s...", download_count, self.mount_name) + source.copy(dest, self.progress_callback, flags, + cancellable=self.cancel_copy) + backup_succeeded = True + logger.debug("...backing up file %s on device %s succeeded", download_count, self.mount_name) + except gio.Error, inst: + fileNotBackedUpMessageDisplayed = True + rpd_file.add_problem(None, pn.BACKUP_ERROR, self.mount_name) + rpd_file.add_extra_detail('%s%s' % (pn.BACKUP_ERROR, self.mount_name), inst) + rpd_file.error_title = _('Backing up error') + rpd_file.error_msg = \ + _("Source: %(source)s\nDestination: %(destination)s") % \ + {'source': rpd_file.download_full_file_name, 'destination': backup_full_file_name} + "\n" + \ + _("Error: %(inst)s") % {'inst': inst} + logger.error("%s:\n%s", rpd_file.error_title, rpd_file.error_msg) + else: + try: + logger.debug("Using python to back up file %s on device %s...", download_count, self.mount_name) + shutil.copy(rpd_file.download_full_file_name, backup_full_file_name) + backup_succeeded = True + logger.debug("...backing up file %s on device %s succeeded", download_count, self.mount_name) + except: + logger.error("Backup of %s failed", backup_full_file_name) + if not backup_succeeded: if rpd_file.status == config.STATUS_DOWNLOAD_FAILED: @@ -197,17 +216,17 @@ class BackupFiles(multiprocessing.Process): if rpd_file.download_xmp_full_name: self.backup_additional_file(dest_dir, rpd_file.download_xmp_full_name) - + self.total_downloaded += rpd_file.size bytes_not_downloaded = rpd_file.size - self.amount_downloaded if bytes_not_downloaded: self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_BYTES, (self.scan_pid, self.pid, self.total_downloaded, bytes_not_downloaded)))) - + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_FILE, (backup_succeeded, rpd_file)))) - - + + diff --git a/rapid/config.py b/rapid/config.py index d44c9bb..977c342 100644 --- a/rapid/config.py +++ b/rapid/config.py @@ -16,7 +16,7 @@ ### Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 ### USA -version = '0.4.5' +version = '0.4.6' GCONF_KEY="/apps/rapid-photo-downloader" @@ -30,9 +30,9 @@ MEDIA_LOCATION = "/media" SKIP_DOWNLOAD = "skip download" ADD_UNIQUE_IDENTIFIER = "add unique identifier" -# These next three values are fall back values that are used only +# These next three values are fall back values that are used only # if calls to xdg-user-dir fail -DEFAULT_PHOTO_LOCATIONS = ['Pictures', 'Photos'] +DEFAULT_PHOTO_LOCATIONS = ['Pictures', 'Photos'] DEFAULT_BACKUP_LOCATION = 'Pictures' DEFAULT_VIDEO_BACKUP_LOCATION = 'Videos' diff --git a/rapid/generatename.py b/rapid/generatename.py index 84c9310..6d138e8 100644 --- a/rapid/generatename.py +++ b/rapid/generatename.py @@ -43,18 +43,18 @@ class PhotoName: Generate the name of a photo. Used as a base class for generating names of videos, as well as subfolder names for both file types """ - + def __init__(self, pref_list): self.pref_list = pref_list - + # Some of the next values are overwritten in derived classes self.strip_initial_period_from_extension = False self.strip_forward_slash = True self.L1_date_check = IMAGE_DATE #used in _get_date_component() self.component = pn.FILENAME_COMPONENT #used in error reporting - - + + def _get_values_from_pref_list(self): for i in range(0, len(self.pref_list), 3): yield (self.pref_list[i], self.pref_list[i+1], self.pref_list[i+2]) @@ -64,19 +64,19 @@ class PhotoName: Returns portion of new file / subfolder name based on date time. If the date is missing, will attempt to use the fallback date. """ - + # step 1: get the correct value from metadata if self.L1 == self.L1_date_check: if self.L2 == SUBSECONDS: - d = self.rpd_file.metadata.sub_seconds() - if d == '00': + d = self.rpd_file.metadata.sub_seconds(missing=None) + if d is None: self.rpd_file.problem.add_problem(self.component, pn.MISSING_METADATA, _(self.L2)) return '' else: return d else: d = self.rpd_file.metadata.date_time(missing=None) - + elif self.L1 == TODAY: d = datetime.datetime.now() elif self.L1 == YESTERDAY: @@ -86,7 +86,7 @@ class PhotoName: d = self.rpd_file.download_start_time else: raise("Date options invalid") - + # step 2: if have a value, try to convert it to string format if d: try: @@ -105,14 +105,14 @@ class PhotoName: else: self.rpd_file.add_problem(self.component, pn.MISSING_METADATA, _(self.L1)) return '' - + try: return d.strftime(convert_date_for_strftime(self.L2)) except: self.rpd_file.add_problem(self.component, pn.INVALID_DATE_TIME, d) logger.error("Both file modification time and metadata date & time are invalid for file %s", self.rpd_file.full_file_name) return '' - + def _get_thm_extension(self): """ Generates THM extension with correct capitalization, if needed @@ -126,7 +126,7 @@ class PhotoName: self.rpd_file.thm_extension = thm_extension else: self.rpd_file.thm_extension = None - + def _get_xmp_extension(self, extension): """ Generates XMP extension with correct capitalization, if needed. @@ -144,15 +144,15 @@ class PhotoName: self.rpd_file.xmp_extension = '.xmp' else: self.rpd_file.xmp_extension = None - - + + def _get_filename_component(self): """ Returns portion of new file / subfolder name based on the file name """ - + name, extension = os.path.splitext(self.rpd_file.name) - + if self.L1 == NAME_EXTENSION: filename = self.rpd_file.name self._get_thm_extension() @@ -178,10 +178,10 @@ class PhotoName: n = re.search("(?P<image_number>[0-9]+$)", name) if not n: self.rpd_file.add_problem(self.component, pn.MISSING_IMAGE_NUMBER) - return '' + return '' else: image_number = n.group("image_number") - + if self.L2 == IMAGE_NUMBER_ALL: filename = image_number elif self.L2 == IMAGE_NUMBER_1: @@ -201,14 +201,14 @@ class PhotoName: filename = filename.lower() return filename - + def _get_metadata_component(self): """ Returns portion of new image / subfolder name based on the metadata - + Note: date time metadata found in _getDateComponent() """ - + if self.L1 == APERTURE: v = self.rpd_file.metadata.aperture() elif self.L1 == ISO: @@ -247,7 +247,7 @@ class PhotoName: else: raise TypeError("Invalid metadata option specified") if self.L1 in [CAMERA_MAKE, CAMERA_MODEL, SHORT_CAMERA_MODEL, - SHORT_CAMERA_MODEL_HYPHEN, OWNER_NAME, ARTIST, + SHORT_CAMERA_MODEL_HYPHEN, OWNER_NAME, ARTIST, COPYRIGHT]: if self.L2 == UPPERCASE: v = v.upper() @@ -269,49 +269,49 @@ class PhotoName: x= x / 26 - 1 v = string.lowercase[r] + v v = string.lowercase[x] + v - + return v - - + + v = _letters(sequence) if self.L2 == UPPERCASE: v = v.upper() - + return v def _format_sequence_no(self, value, amountToPad): padding = LIST_SEQUENCE_NUMBERS_L2.index(amountToPad) + 1 formatter = '%0' + str(padding) + "i" return formatter % value - + def _get_downloads_today(self): return self._format_sequence_no(self.rpd_file.sequences.get_downloads_today(), self.L2) def _get_session_sequence_no(self): - return self._format_sequence_no(self.rpd_file.sequences.get_session_sequence_no(), self.L2) - + return self._format_sequence_no(self.rpd_file.sequences.get_session_sequence_no(), self.L2) + def _get_stored_sequence_no(self): return self._format_sequence_no(self.rpd_file.sequences.get_stored_sequence_no(), self.L2) def _get_sequence_letter(self): return self._calculate_letter_sequence(self.rpd_file.sequences.get_sequence_letter()) - + def _get_sequences_component(self): if self.L1 == DOWNLOAD_SEQ_NUMBER: - return self._get_downloads_today() + return self._get_downloads_today() elif self.L1 == SESSION_SEQ_NUMBER: return self._get_session_sequence_no() elif self.L1 == STORED_SEQ_NUMBER: - return self._get_stored_sequence_no() + return self._get_stored_sequence_no() elif self.L1 == SEQUENCE_LETTER: - return self._get_sequence_letter() + return self._get_sequence_letter() #~ elif self.L1 == SUBFOLDER_SEQ_NUMBER: #~ return self._getSubfolderSequenceNo() - - - + + + def _get_component(self): try: if self.L0 == DATE_TIME: @@ -331,8 +331,8 @@ class PhotoName: except: self.rpd_file.add_problem(self.component, pn.ERROR_IN_GENERATION, _(self.L0)) return '' - - + + def generate_name(self, rpd_file): self.rpd_file = rpd_file @@ -342,89 +342,89 @@ class PhotoName: v = self._get_component() if v: name += v - + # remove any null characters - they are bad news in filenames name = name.replace('\x00', '') if self.rpd_file.strip_characters: for c in r'\:*?"<>|': name = name.replace(c, '') - + if self.strip_forward_slash: name = name.replace('/', '') - + name = name.strip() - + return name - - + + class VideoName(PhotoName): def __init__(self, pref_list): PhotoName.__init__(self, pref_list) self.L1_date_check = VIDEO_DATE #used in _get_date_component() - + def _get_metadata_component(self): """ Returns portion of video / subfolder name based on the metadata - + Note: date time metadata found in _getDateComponent() """ - return get_video_metadata_component(self) + return get_video_metadata_component(self) class PhotoSubfolder(PhotoName): """ Generate subfolder names for photo files """ - + def __init__(self, pref_list): self.pref_list = pref_list - + self.strip_extraneous_white_space = re.compile(r'\s*%s\s*' % os.sep) self.strip_initial_period_from_extension = True self.strip_forward_slash = False self.L1_date_check = IMAGE_DATE #used in _get_date_component() self.component = pn.SUBFOLDER_COMPONENT #used in error reporting - + def generate_name(self, rpd_file): - + subfolders = PhotoName.generate_name(self, rpd_file) - - # subfolder value must never start with a separator, or else any - # os.path.join function call will fail to join a subfolder to its + + # subfolder value must never start with a separator, or else any + # os.path.join function call will fail to join a subfolder to its # parent folder if subfolders: if subfolders[0] == os.sep: subfolders = subfolders[1:] - + # remove any spaces before and after a directory name if subfolders and self.rpd_file.strip_characters: subfolders = self.strip_extraneous_white_space.sub(os.sep, subfolders) - + return subfolders - - - + + + class VideoSubfolder(PhotoSubfolder): """ Generate subfolder names for video files """ - + def __init__(self, pref_list): PhotoSubfolder.__init__(self, pref_list) self.L1_date_check = VIDEO_DATE #used in _get_date_component() - - + + def _get_metadata_component(self): """ Returns portion of video / subfolder name based on the metadata - + Note: date time metadata found in _getDateComponent() """ - return get_video_metadata_component(self) - + return get_video_metadata_component(self) + def get_video_metadata_component(video): """ Returns portion of video / subfolder name based on the metadata @@ -432,7 +432,7 @@ def get_video_metadata_component(video): This is outside of a class definition because of the inheritence hierarchy. """ - + problem = None if video.L1 == CODEC: v = video.rpd_file.metadata.codec() @@ -456,7 +456,7 @@ def get_video_metadata_component(video): return v class Sequences: - """ + """ Holds sequence numbers and letters used in generating filenames. """ def __init__(self, downloads_today_tracker, stored_sequence_no): @@ -465,58 +465,58 @@ class Sequences: self.downloads_today_tracker = downloads_today_tracker self.stored_sequence_no = stored_sequence_no self.matched_sequences = None - + def set_matched_sequence_value(self, matched_sequences): self.matched_sequences = matched_sequences - + def get_session_sequence_no(self): if self.matched_sequences is not None: return self.matched_sequences.session_sequence_no else: return self._get_session_sequence_no() - + def _get_session_sequence_no(self): return self.session_sequence_no + 1 - + def get_sequence_letter(self): if self.matched_sequences is not None: return self.matched_sequences.sequence_letter else: return self._get_sequence_letter() - + def _get_sequence_letter(self): return self.sequence_letter + 1 - + def increment(self, uses_session_sequece_no, uses_sequence_letter): if uses_session_sequece_no: self.session_sequence_no += 1 if uses_sequence_letter: self.sequence_letter += 1 - + def get_downloads_today(self): if self.matched_sequences is not None: return self.matched_sequences.downloads_today else: return self._get_downloads_today() - + def _get_downloads_today(self): v = self.downloads_today_tracker.get_downloads_today() if v == -1: return 1 else: return v + 1 - + def get_stored_sequence_no(self): if self.matched_sequences is not None: return self.matched_sequences.stored_sequence_no else: return self._get_stored_sequence_no() - + def _get_stored_sequence_no(self): # Must add 1 to the value, for historic reasons (that is how it used # to work) return self.stored_sequence_no + 1 - + def create_matched_sequences(self): sequences = collections.namedtuple( 'AssignedSequences', diff --git a/rapid/glade3/about.ui b/rapid/glade3/about.ui index b770f72..221c62e 100644 --- a/rapid/glade3/about.ui +++ b/rapid/glade3/about.ui @@ -28,6 +28,7 @@ Martin Dahl Moe Marco de Freitas <marcodefreitas@gmail.com> Martin Egger <martin.egger@gmx.net> Tauno Erik <tauno.erik@gmail.com> +Sergiy Gavrylov <sergiovana@bigmir.net> Emanuele Grande <caccolangrifata@gmail.com> Torben Gundtofte-Bruun <torben@g-b.dk> Miroslav Matejaš <silverspace@ubuntu-hr.org> @@ -43,7 +44,7 @@ Michal Predotka <mpredotka@googlemail.com> Ye Qing <allen19920930@gmail.com> Luca Reverberi <thereve@gmail.com> Mikko Ruohola <polarfox@polarfox.net> -Sergiy Gavrylov <sergiovana@bigmir.net> +Ahmed Shubbar <ahmed.shubbar@gmail.com> Sergei Sedov <sedov@webmail.perm.ru> Marco Solari <marcosolari@gmail.com> Toni Lähdekorpi <toni@lygon.net> diff --git a/rapid/glade3/xmp.ui b/rapid/glade3/xmp.ui new file mode 100644 index 0000000..c0fd03b --- /dev/null +++ b/rapid/glade3/xmp.ui @@ -0,0 +1,1376 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="2.24"/> + <!-- interface-naming-policy project-wide --> + <object class="GtkDialog" id="dialog1"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="type_hint">dialog</property> + <child internal-child="vbox"> + <object class="GtkVBox" 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"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkNotebook" id="notebook"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkTable" id="table1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">10</property> + <property name="n_columns">5</property> + <child> + <object class="GtkLabel" id="label14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + <property name="label" translatable="yes" comments="The Description field, often referred to as "Caption," should report the who, what and why of what the photograph depicts. Source: http://www.photometadata.org/META-Resources-Field-Guide-to-Metadata#Description">Description:</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <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="GtkViewport" id="viewport1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkTextView" id="textview1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label15"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="A headline is a brief, publishable synopsis or summary of the contents of the photograph. Source: http://www.photometadata.org/META-Resources-Field-Guide-to-Metadata#Headline">Headline:</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label16"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + <property name="label" translatable="yes" comments="Keyword terms or phrases describe the subject of content in the photograph. Source: http://www.photometadata.org/META-Resources-Field-Guide-to-Metadata#Keywords">Keywords:</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow3"> + <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="GtkViewport" id="viewport3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkTextView" id="textview2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label17"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="The name of the person writing, editing or correcting the description of the photograph. Source: http://www.photometadata.org/META-Resources-Field-Guide-to-Metadata#Description%20writer">Description writer:</property> + </object> + <packing> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label18"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="The Copyright Notice for the photo.">Copyright:</property> + </object> + <packing> + <property name="top_attach">5</property> + <property name="bottom_attach">6</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry3"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">5</property> + <property name="bottom_attach">6</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label19"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="The location of a web page describing the owner and/or rights statement for this photo.">Copyright URL:</property> + </object> + <packing> + <property name="top_attach">6</property> + <property name="bottom_attach">7</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry4"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="invisible_char_set">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">6</property> + <property name="bottom_attach">7</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label20"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + <property name="label" translatable="yes" comments="Name or names of a person or the people shown in the photo.">Person(s) shown:</property> + </object> + <packing> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow4"> + <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="GtkViewport" id="viewport4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkTextView" id="textview3"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + <child type="tab"> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Description</property> + </object> + <packing> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">16</property> + <property name="n_columns">5</property> + <child> + <object class="GtkLabel" id="label21"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Location Created</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="right_attach">5</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label22"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Sublocation:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label23"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">City:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label24"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Province/state:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label25"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Country name:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label26"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Country ISO-code:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">5</property> + <property name="bottom_attach">6</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label27"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">World region:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">6</property> + <property name="bottom_attach">7</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry5"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="invisible_char_set">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry6"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="invisible_char_set">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry7"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="invisible_char_set">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry8"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="invisible_char_set">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry9"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="invisible_char_set">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">5</property> + <property name="bottom_attach">6</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry10"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="invisible_char_set">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">6</property> + <property name="bottom_attach">7</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label28"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Location Shown</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="right_attach">5</property> + <property name="top_attach">8</property> + <property name="bottom_attach">9</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label29"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Sublocation:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</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> + <object class="GtkLabel" id="label30"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">City:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">10</property> + <property name="bottom_attach">11</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label31"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Province/state:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">11</property> + <property name="bottom_attach">12</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label32"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Country name:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">12</property> + <property name="bottom_attach">13</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label33"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Country ISO-code:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">13</property> + <property name="bottom_attach">14</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label34"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">World region:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">14</property> + <property name="bottom_attach">15</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry11"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="invisible_char_set">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">9</property> + <property name="bottom_attach">10</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry12"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="invisible_char_set">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">10</property> + <property name="bottom_attach">11</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry13"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="invisible_char_set">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">11</property> + <property name="bottom_attach">12</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry14"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="invisible_char_set">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">12</property> + <property name="bottom_attach">13</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry15"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="invisible_char_set">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">13</property> + <property name="bottom_attach">14</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry16"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="invisible_char_set">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">14</property> + <property name="bottom_attach">15</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <object class="GtkButton" id="copy_location_button"> + <property name="label" translatable="yes">Copy to Location Shown</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> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">7</property> + <property name="bottom_attach">8</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label35"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Location</property> + </object> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">10</property> + <property name="n_columns">2</property> + <property name="column_spacing">12</property> + <property name="row_spacing">6</property> + <child> + <object class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Person who created the photograph">Creator:</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Job title of the person who created the photograph">Creator's job title:</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + <property name="label" translatable="yes" comments="Address of the person who created the photograph">Address:</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">City:</property> + </object> + <packing> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="State or province of the person who created the photograph">State/province:</property> + </object> + <packing> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Postal code of the person who created the photograph">Postal code:</property> + </object> + <packing> + <property name="top_attach">5</property> + <property name="bottom_attach">6</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Country of the person who created the photograph">Country: </property> + </object> + <packing> + <property name="top_attach">6</property> + <property name="bottom_attach">7</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Phone numbers / number of the person who created the photograph">Phone(s):</property> + </object> + <packing> + <property name="top_attach">7</property> + <property name="bottom_attach">8</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Email address / addresses of the person who created the photograph">Email(s):</property> + </object> + <packing> + <property name="top_attach">8</property> + <property name="bottom_attach">9</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Websites / website of the person who created the photograph">Website(s):</property> + </object> + <packing> + <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> + <object class="GtkComboBox" id="creator_combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="has_entry">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="job_title_combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="has_entry">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="city_combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="has_entry">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="state_combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="has_entry">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="postal_code_combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="has_entry">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">5</property> + <property name="bottom_attach">6</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="country_combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="has_entry">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">6</property> + <property name="bottom_attach">7</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="phone_combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="has_entry">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">7</property> + <property name="bottom_attach">8</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="email_combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="has_entry">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">8</property> + <property name="bottom_attach">9</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="website_combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="has_entry">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">9</property> + <property name="bottom_attach">10</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow2"> + <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="GtkViewport" id="viewport2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkTextView" id="address_textview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + </packing> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Contact information for the photographer">Photographer</property> + </object> + <packing> + <property name="position">2</property> + <property name="tab_fill">False</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + </object> +</interface> diff --git a/rapid/rapid.py b/rapid/rapid.py index fab5796..7de18c2 100755 --- a/rapid/rapid.py +++ b/rapid/rapid.py @@ -18,6 +18,7 @@ ### Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 ### USA +use_pynotify = True import tempfile @@ -39,7 +40,9 @@ import webbrowser import sys, time, types, os, datetime import gobject, pango, cairo, array, pangocairo, gio -import pynotify + +if use_pynotify: + import pynotify from multiprocessing import Process, Pipe, Queue, Event, Value, Array, current_process, log_to_stderr from ctypes import c_int, c_bool, c_char @@ -50,7 +53,7 @@ logger = log_to_stderr() # Rapid Photo Downloader modules import rpdfile - + import problemnotification as pn import thumbnail as tn import rpdmultiprocessing as rpdmp @@ -105,10 +108,10 @@ from config import STATUS_CANNOT_DOWNLOAD, STATUS_DOWNLOADED, \ STATUS_NOT_DOWNLOADED, \ STATUS_DOWNLOAD_AND_BACKUP_FAILED, \ STATUS_WARNING - + DOWNLOADED = [STATUS_DOWNLOADED, STATUS_DOWNLOADED_WITH_WARNING, STATUS_BACKUP_PROBLEM] -#Translators: if neccessary, for guidance in how to translate this program, you may see http://damonlynch.net/translate.html +#Translators: if neccessary, for guidance in how to translate this program, you may see http://damonlynch.net/translate.html PROGRAM_NAME = _('Rapid Photo Downloader') __version__ = config.version @@ -117,12 +120,12 @@ def date_time_human_readable(date, with_line_break=True): return _("%(date)s\n%(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")} else: return _("%(date)s %(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")} - + def date_time_subseconds_human_readable(date, subseconds): return _("%(date)s %(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \ - {'date':date.strftime("%x"), + {'date':date.strftime("%x"), 'hour':date.strftime("%H"), - 'minute':date.strftime("%M"), + 'minute':date.strftime("%M"), 'second':date.strftime("%S"), 'subsecond': subseconds} @@ -135,7 +138,7 @@ class DeviceCollection(gtk.TreeView): def __init__(self, parent_app): self.parent_app = parent_app - # device icon & name, size of images on the device (human readable), + # device icon & name, size of images on the device (human readable), # copy progress (%), copy text, eject button (None if irrelevant), # process id, pulse self.liststore = gtk.ListStore(gtk.gdk.Pixbuf, str, str, float, str, @@ -144,15 +147,15 @@ class DeviceCollection(gtk.TreeView): self.devices_by_scan_pid = {} gtk.TreeView.__init__(self, self.liststore) - + self.props.enable_search = False # make it impossible to select a row selection = self.get_selection() selection.set_mode(gtk.SELECTION_NONE) self.set_headers_visible(False) - - - # Device refers to a thing like a camera, memory card in its reader, + + + # Device refers to a thing like a camera, memory card in its reader, # external hard drive, Portable Storage Device, etc. column0 = gtk.TreeViewColumn(_("Device")) pixbuf_renderer = gtk.CellRendererPixbuf() @@ -168,46 +171,46 @@ class DeviceCollection(gtk.TreeView): column0.add_attribute(text_renderer, 'text', 1) column0.add_attribute(eject_renderer, 'pixbuf', 5) self.append_column(column0) - - + + # Size refers to the total size of images on the device, typically in # MB or GB column1 = gtk.TreeViewColumn(_("Size"), gtk.CellRendererText(), text=2) self.append_column(column1) - - column2 = gtk.TreeViewColumn(_("Download Progress"), + + column2 = gtk.TreeViewColumn(_("Download Progress"), gtk.CellRendererProgress(), value=3, text=4, pulse=7) self.append_column(column2) self.show_all() - + icontheme = gtk.icon_theme_get_default() try: - self.eject_pixbuf = icontheme.load_icon('media-eject', 16, + self.eject_pixbuf = icontheme.load_icon('media-eject', 16, gtk.ICON_LOOKUP_USE_BUILTIN) except: self.eject_pixbuf = gtk.gdk.pixbuf_new_from_file( paths.share_dir('glade3/media-eject.png')) - + self.add_events(gtk.gdk.BUTTON_PRESS_MASK) self.connect('button-press-event', self.button_clicked) - + def add_device(self, process_id, device, progress_bar_text = ''): - + # add the row, and get a temporary pointer to the row size_files = '' progress = 0.0 - + if device.mount is None: eject = None else: eject = self.eject_pixbuf - + self.devices_by_scan_pid[process_id] = device - + iter = self.liststore.append((device.get_icon(), device.get_name(), size_files, @@ -216,9 +219,9 @@ class DeviceCollection(gtk.TreeView): eject, process_id, -1)) - + self._set_process_map(process_id, iter) - + # adjust scrolled window height, based on row height and number of ready to start downloads # please note, at program startup, self.row_height() will be less than it will be when already running @@ -228,7 +231,8 @@ class DeviceCollection(gtk.TreeView): row_height = self.get_background_area(0, self.get_column(0))[3] + 1 height = max(((len(self.map_process_to_row) + 1) * row_height), 24) self.parent_app.device_collection_scrolledwindow.set_size_request(-1, height) - + + def update_device(self, process_id, total_size_files): """ Updates the size of the photos and videos on the device, displayed to the user @@ -238,39 +242,39 @@ class DeviceCollection(gtk.TreeView): self.liststore.set_value(iter, 2, total_size_files) else: logger.critical("This device is unknown") - + def get_device(self, process_id): return self.devices_by_scan_pid.get(process_id) - + def remove_device(self, process_id): if process_id in self.map_process_to_row: iter = self._get_process_map(process_id) self.liststore.remove(iter) del self.map_process_to_row[process_id] del self.devices_by_scan_pid[process_id] - + def get_all_displayed_processes(self): """ - returns a list of the processes currently being displayed to the user + returns a list of the processes currently being displayed to the user """ return self.map_process_to_row.keys() def _set_process_map(self, process_id, iter): """ - convert the temporary iter into a tree reference, which is + convert the temporary iter into a tree reference, which is permanent """ path = self.liststore.get_path(iter) treerowref = gtk.TreeRowReference(self.liststore, path) self.map_process_to_row[process_id] = treerowref - + def _get_process_map(self, process_id): """ return the tree iter for this process """ - + if process_id in self.map_process_to_row: treerowref = self.map_process_to_row[process_id] path = treerowref.get_path() @@ -278,16 +282,16 @@ class DeviceCollection(gtk.TreeView): return iter else: return None - + def update_progress(self, scan_pid, percent_complete, progress_bar_text, bytes_downloaded, pulse=None): - + iter = self._get_process_map(scan_pid) if iter: if percent_complete: self.liststore.set_value(iter, 3, percent_complete) if progress_bar_text: self.liststore.set_value(iter, 4, progress_bar_text) - + if pulse is not None: if pulse: # Make the bar pulse @@ -316,14 +320,14 @@ class DeviceCollection(gtk.TreeView): iter = self.liststore.get_iter(path) if self.liststore.get_value(iter, 5) is not None: self.unmount(process_id = self.liststore.get_value(iter, 6)) - + def unmount(self, process_id): device = self.devices_by_scan_pid[process_id] if device.mount is not None: logger.debug("Unmounting device with scan pid %s", process_id) device.mount.unmount(self.unmount_callback) - - + + def unmount_callback(self, mount, result): name = mount.get_name() @@ -332,13 +336,14 @@ class DeviceCollection(gtk.TreeView): logger.debug("%s successfully unmounted" % name) except gio.Error, inst: logger.error("%s did not unmount: %s", name, inst) - - title = _("%(device)s did not unmount") % {'device': name} - message = '%s' % inst - - n = pynotify.Notification(title, message) - n.set_icon_from_pixbuf(self.parent_app.application_icon) - n.show() + + if use_pynotify: + title = _("%(device)s did not unmount") % {'device': name} + message = '%s' % inst + + n = pynotify.Notification(title, message) + n.set_icon_from_pixbuf(self.parent_app.application_icon) + n.show() def create_cairo_image_surface(pil_image, image_width, image_height): @@ -353,55 +358,55 @@ class ThumbnailCellRenderer(gtk.CellRenderer): __gproperties__ = { "image": (gobject.TYPE_PYOBJECT, "Image", "Image", gobject.PARAM_READWRITE), - - "filename": (gobject.TYPE_STRING, "Filename", + + "filename": (gobject.TYPE_STRING, "Filename", "Filename", '', gobject.PARAM_READWRITE), - + "status": (gtk.gdk.Pixbuf, "Status", "Status", gobject.PARAM_READWRITE), } - + def __init__(self, checkbutton_height): gtk.CellRenderer.__init__(self) self.image = None - + self.image_area_size = 100 self.text_area_size = 30 self.padding = 6 self.checkbutton_height = checkbutton_height self.icon_width = 20 - + def do_set_property(self, pspec, value): setattr(self, pspec.name, value) def do_get_property(self, pspec): return getattr(self, pspec.name) - + def do_render(self, window, widget, background_area, cell_area, expose_area, flags): - + cairo_context = window.cairo_create() - + x = cell_area.x y = cell_area.y + self.checkbutton_height - 8 w = cell_area.width h = cell_area.height - - #constrain operations to cell area, allowing for a 1 pixel border + + #constrain operations to cell area, allowing for a 1 pixel border #either side #~ cairo_context.rectangle(x-1, y-1, w+2, h+2) #~ cairo_context.clip() - + #fill in the background with dark grey #this ensures that a selected cell's fill does not make #the text impossible to read #~ cairo_context.rectangle(x, y, w, h) #~ cairo_context.set_source_rgb(0.267, 0.267, 0.267) #~ cairo_context.fill() - + #image width and height image_w = self.image.size[0] image_h = self.image.size[1] - + #center the image horizontally #bottom align vertically #top left and right corners for the image: @@ -416,33 +421,33 @@ class ThumbnailCellRenderer(gtk.CellRenderer): cairo_context.set_line_width(1) cairo_context.rectangle(image_x-.5, image_y-.5, image_w+1, image_h+1) cairo_context.stroke() - + # draw a thin border around each cell #~ cairo_context.set_source_rgb(0.33,0.33,0.33) #~ cairo_context.rectangle(x, y, w, h) #~ cairo_context.stroke() - + #place the image cairo_context.set_source_surface(image, image_x, image_y) cairo_context.paint() - + #text context = pangocairo.CairoContext(cairo_context) - + text_y = y + self.image_area_size + 10 text_w = w - self.icon_width text_x = x + self.icon_width #~ context.rectangle(text_x, text_y, text_w, 15) - #~ context.clip() - + #~ context.clip() + layout = context.create_layout() width = text_w * pango.SCALE layout.set_width(width) - + layout.set_alignment(pango.ALIGN_CENTER) layout.set_ellipsize(pango.ELLIPSIZE_END) - + #font color and size fg_color = pango.AttrForeground(65535, 65535, 65535, 0, -1) font_size = pango.AttrSize(8192, 0, -1) # 8 * 1024 = 8192 @@ -453,7 +458,7 @@ class ThumbnailCellRenderer(gtk.CellRenderer): attr.insert(font_family) layout.set_attributes(attr) - layout.set_text(self.filename) + layout.set_text(self.filename) context.move_to(text_x, text_y) context.show_layout(layout) @@ -461,13 +466,13 @@ class ThumbnailCellRenderer(gtk.CellRenderer): #status cairo_context.set_source_pixbuf(self.status, x, y + self.image_area_size + 10) cairo_context.paint() - + def do_get_size(self, widget, cell_area): return (0, 0, self.image_area_size, self.image_area_size + self.text_area_size - self.checkbutton_height + 4) - + gobject.type_register(ThumbnailCellRenderer) - + class ThumbnailDisplay(gtk.IconView): def __init__(self, parent_app): @@ -475,39 +480,39 @@ class ThumbnailDisplay(gtk.IconView): self.set_spacing(0) self.set_row_spacing(5) self.set_margin(25) - + self.set_selection_mode(gtk.SELECTION_MULTIPLE) self.connect('selection-changed', self.on_selection_changed) self._selected_items = [] - + self.rapid_app = parent_app - + self.batch_size = 10 - + self.thumbnail_manager = ThumbnailManager(self.thumbnail_results, self.batch_size) self.preview_manager = PreviewManager(self.preview_results) - - self.treerow_index = {} - self.process_index = {} - + + self.treerow_index = {} + self.process_index = {} + self.rpd_files = {} - + self.total_thumbs_to_generate = 0 self.thumbnails_generated = 0 - + # dict of scan_pids that are having thumbnails generated # value is the thumbnail process id # this is needed when terminating thumbnailing early such as when # user clicks download before the thumbnailing is finished self.generating_thumbnails = {} - + self.thumbnails = {} self.previews = {} self.previews_being_fetched = set() - + self.stock_photo_thumbnails = tn.PhotoIcons() self.stock_video_thumbnails = tn.VideoIcons() - + self.SELECTED_COL = 1 self.UNIQUE_ID_COL = 2 self.TIMESTAMP_COL = 4 @@ -515,11 +520,11 @@ class ThumbnailDisplay(gtk.IconView): self.CHECKBUTTON_VISIBLE_COL = 6 self.DOWNLOAD_STATUS_COL = 7 self.STATUS_ICON_COL = 8 - + self._create_liststore() self.clear() - + checkbutton = gtk.CellRendererToggle() checkbutton.set_radio(False) checkbutton.props.activatable = True @@ -529,11 +534,11 @@ class ThumbnailDisplay(gtk.IconView): self.add_attribute(checkbutton, "active", 1) self.add_attribute(checkbutton, "visible", 6) - + checkbutton_size = checkbutton.get_size(self, None) checkbutton_height = checkbutton_size[3] checkbutton_width = checkbutton_size[2] - + image = ThumbnailCellRenderer(checkbutton_height) self.pack_start(image, expand=True) self.add_attribute(image, "image", 0) @@ -542,12 +547,12 @@ class ThumbnailDisplay(gtk.IconView): #set the background color to a darkish grey self.modify_base(gtk.STATE_NORMAL, gtk.gdk.Color('#444444')) - + self.show_all() self._setup_icons() - + self.connect('item-activated', self.on_item_activated) - + def _create_liststore(self): """ Creates the default list store to hold the icons @@ -562,8 +567,8 @@ class ThumbnailDisplay(gtk.IconView): gobject.TYPE_BOOLEAN, # 6 visibility of checkbutton int, # 7 status of download gtk.gdk.Pixbuf, # 8 status icon - ) - + ) + def _setup_icons(self): # icons to be displayed in status column @@ -582,19 +587,19 @@ class ThumbnailDisplay(gtk.IconView): size, size) self.download_pending_icon = gtk.gdk.pixbuf_new_from_file_at_size( paths.share_dir('glade3/rapid-photo-downloader-download-pending.png'), - size, size) + size, size) self.downloaded_with_warning_icon = gtk.gdk.pixbuf_new_from_file_at_size( paths.share_dir('glade3/rapid-photo-downloader-downloaded-with-warning.svg'), size, size) self.downloaded_with_error_icon = gtk.gdk.pixbuf_new_from_file_at_size( paths.share_dir('glade3/rapid-photo-downloader-downloaded-with-error.svg'), size, size) - + # make the not yet downloaded icon a transparent square self.not_downloaded_icon = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, 16, 16) self.not_downloaded_icon.fill(0xffffffff) self.not_downloaded_icon = self.not_downloaded_icon.add_alpha(True, chr(255), chr(255), chr(255)) - + def get_status_icon(self, status): """ Returns the correct icon, based on the status @@ -616,33 +621,33 @@ class ThumbnailDisplay(gtk.IconView): else: logger.critical("FIXME: unknown status: %s", status) status_icon = self.not_downloaded_icon - return status_icon - + return status_icon + def sort_by_timestamp(self): self.liststore.set_sort_column_id(self.TIMESTAMP_COL, gtk.SORT_ASCENDING) - + def on_selection_changed(self, iconview): self._selected_items = self.get_selected_items() - + def on_checkbutton_toggled(self, cellrenderertoggle, path): paths = [p[0] for p in self._selected_items] if int(path) not in paths: self._selected_items = [path,] - + for path in self._selected_items: iter = self.liststore.get_iter(path) status = self.liststore.get_value(iter, self.DOWNLOAD_STATUS_COL) if status == STATUS_NOT_DOWNLOADED: self.liststore.set_value(iter, self.SELECTED_COL, not cellrenderertoggle.get_active()) self.select_path(path) - + self.rapid_app.set_download_action_sensitivity() - - + + def set_selected(self, unique_id, value): iter = self.get_iter_from_unique_id(unique_id) self.liststore.set_value(iter, self.SELECTED_COL, value) - + def add_file(self, rpd_file, generate_thumbnail): thumbnail_icon = self.get_stock_icon(rpd_file.file_type) @@ -650,7 +655,7 @@ class ThumbnailDisplay(gtk.IconView): scan_pid = rpd_file.scan_pid timestamp = int(rpd_file.modification_time) - + iter = self.liststore.append((thumbnail_icon, True, unique_id, @@ -661,47 +666,47 @@ class ThumbnailDisplay(gtk.IconView): STATUS_NOT_DOWNLOADED, self.not_downloaded_icon )) - + path = self.liststore.get_path(iter) treerowref = gtk.TreeRowReference(self.liststore, path) - + if scan_pid in self.process_index: self.process_index[scan_pid].append(unique_id) else: self.process_index[scan_pid] = [unique_id,] - + self.treerow_index[unique_id] = treerowref self.rpd_files[unique_id] = rpd_file - + if generate_thumbnail: self.total_thumbs_to_generate += 1 def get_sample_file(self, file_type): - """Returns an rpd_file for of a given file type, or None if it does + """Returns an rpd_file for of a given file type, or None if it does not exist""" for unique_id, rpd_file in self.rpd_files.iteritems(): if rpd_file.file_type == file_type: if rpd_file.status <> STATUS_CANNOT_DOWNLOAD: return rpd_file - + return None - + def get_unique_id_from_iter(self, iter): return self.liststore.get_value(iter, 2) - + def get_iter_from_unique_id(self, unique_id): treerowref = self.treerow_index[unique_id] path = treerowref.get_path() return self.liststore.get_iter(path) - - def on_item_activated(self, iconview, path): + + def on_item_activated(self, iconview, path): """ """ iter = self.liststore.get_iter(path) self.show_preview(iter=iter) self.advance_get_preview_image(iter) - + def _get_preview(self, unique_id, rpd_file): if unique_id not in self.previews_being_fetched: #check if preview should be from a downloaded file, or the source @@ -711,13 +716,13 @@ class ThumbnailDisplay(gtk.IconView): else: file_location = rpd_file.full_file_name thm_file_name = rpd_file.thm_full_name - + self.preview_manager.get_preview(unique_id, file_location, thm_file_name, rpd_file.file_type, size_max=None,) - + self.previews_being_fetched.add(unique_id) - + def show_preview(self, unique_id=None, iter=None): if unique_id is not None: iter = self.get_iter_from_unique_id(unique_id) @@ -734,31 +739,31 @@ class ThumbnailDisplay(gtk.IconView): path = 0 iter = self.liststore.get_iter(path) unique_id = self.get_unique_id_from_iter(iter) - - - rpd_file = self.rpd_files[unique_id] - + + + rpd_file = self.rpd_files[unique_id] + if unique_id in self.previews: preview_image = self.previews[unique_id] else: # request daemon process to get a full size thumbnail self._get_preview(unique_id, rpd_file) - if unique_id in self.thumbnails: + if unique_id in self.thumbnails: preview_image = self.thumbnails[unique_id] else: preview_image = self.get_stock_icon(rpd_file.file_type) - + checked = self.liststore.get_value(iter, self.SELECTED_COL) include_checkbutton_visible = rpd_file.status == STATUS_NOT_DOWNLOADED - self.rapid_app.show_preview_image(unique_id, preview_image, + self.rapid_app.show_preview_image(unique_id, preview_image, include_checkbutton_visible, checked) - + def _get_next_iter(self, iter): iter = self.liststore.iter_next(iter) if iter is None: iter = self.liststore.get_iter_first() return iter - + def _get_prev_iter(self, iter): row = self.liststore.get_path(iter)[0] if row == 0: @@ -766,44 +771,44 @@ class ThumbnailDisplay(gtk.IconView): else: row -= 1 iter = self.liststore.get_iter(row) - return iter - + return iter + def show_next_image(self, unique_id): iter = self.get_iter_from_unique_id(unique_id) iter = self._get_next_iter(iter) if iter is not None: self.show_preview(iter=iter) - + # cache next image self.advance_get_preview_image(iter, prev=False, next=True) - + def show_prev_image(self, unique_id): iter = self.get_iter_from_unique_id(unique_id) iter = self._get_prev_iter(iter) if iter is not None: self.show_preview(iter=iter) - + # cache next image self.advance_get_preview_image(iter, prev=True, next=False) - + def advance_get_preview_image(self, iter, prev=True, next=True): unique_ids = [] if next: next_iter = self._get_next_iter(iter) unique_ids.append(self.get_unique_id_from_iter(next_iter)) - + if prev: prev_iter = self._get_prev_iter(iter) unique_ids.append(self.get_unique_id_from_iter(prev_iter)) - + for unique_id in unique_ids: if not unique_id in self.previews: rpd_file = self.rpd_files[unique_id] self._get_preview(unique_id, rpd_file) - + def check_all(self, check_all, file_type=None): for row in self.liststore: if row[self.CHECKBUTTON_VISIBLE_COL]: @@ -813,7 +818,7 @@ class ThumbnailDisplay(gtk.IconView): else: row[self.SELECTED_COL] = check_all self.rapid_app.set_download_action_sensitivity() - + def files_are_checked_to_download(self): """ Returns True if there is any file that the user has indicated they @@ -825,12 +830,12 @@ class ThumbnailDisplay(gtk.IconView): if rpd_file.status not in DOWNLOADED: return True return False - + def get_files_checked_for_download(self, scan_pid): """ Returns a dict of scan ids and associated files the user has indicated they want to download - + If scan_pid is not None, then returns only those files from that scan_pid """ files = dict() @@ -853,7 +858,7 @@ class ThumbnailDisplay(gtk.IconView): if self.liststore.get_value(iter, self.SELECTED_COL): files[scan_pid].append(rpd_file) return files - + def get_no_files_remaining(self, scan_pid): """ Returns the number of files that have not yet been downloaded for the @@ -865,17 +870,17 @@ class ThumbnailDisplay(gtk.IconView): if rpd_file.status == STATUS_NOT_DOWNLOADED: i += 1 return i - + def files_remain_to_download(self): """ - Returns True if any files remain that are not downloaded, else returns + Returns True if any files remain that are not downloaded, else returns False """ for row in self.liststore: if row[self.DOWNLOAD_STATUS_COL] == STATUS_NOT_DOWNLOADED: return True return False - + def mark_download_pending(self, files_by_scan_pid): """ @@ -895,19 +900,19 @@ class ThumbnailDisplay(gtk.IconView): self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, STATUS_DOWNLOAD_PENDING) icon = self.get_status_icon(STATUS_DOWNLOAD_PENDING) self.liststore.set_value(iter, self.STATUS_ICON_COL, icon) - + def select_image(self, unique_id): iter = self.get_iter_from_unique_id(unique_id) path = self.liststore.get_path(iter) self.select_path(path) self.scroll_to_path(path, use_align=False, row_align=0.5, col_align=0.5) - + def get_stock_icon(self, file_type): if file_type == rpdfile.FILE_TYPE_PHOTO: return self.stock_photo_thumbnails.stock_thumbnail_image_icon else: return self.stock_video_thumbnails.stock_thumbnail_image_icon - + def update_status_post_download(self, rpd_file): iter = self.get_iter_from_unique_id(rpd_file.unique_id) self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, rpd_file.status) @@ -915,7 +920,7 @@ class ThumbnailDisplay(gtk.IconView): self.liststore.set_value(iter, self.STATUS_ICON_COL, icon) self.liststore.set_value(iter, self.CHECKBUTTON_VISIBLE_COL, False) self.rpd_files[rpd_file.unique_id] = rpd_file - + def generate_thumbnails(self, scan_pid): """Initiate thumbnail generation for files scanned in one process """ @@ -923,81 +928,81 @@ class ThumbnailDisplay(gtk.IconView): 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] path = treerowref.get_path() iter = self.liststore.get_iter(path) - self.liststore.set(iter, 0, icon) - + self.liststore.set(iter, 0, icon) + def update_thumbnail(self, thumbnail_data): """ Takes the generated thumbnail and updates the display - + If the thumbnail_data includes a second image, that is used to update the thumbnail list using the unique_id """ unique_id = thumbnail_data[0] thumbnail_icon = thumbnail_data[1] - + if thumbnail_icon is not None: # get the thumbnail icon in PIL format thumbnail_icon = thumbnail_icon.get_image() - + if thumbnail_icon: self._set_thumbnail(unique_id, thumbnail_icon) - + if len(thumbnail_data) > 2: # get the 2nd image in PIL format self.thumbnails[unique_id] = thumbnail_data[2].get_image() def terminate_thumbnail_generation(self, scan_pid): """ - Terminates thumbnail generation if thumbnails are currently + Terminates thumbnail generation if thumbnails are currently being generated for this scan_pid """ - + if scan_pid in self.generating_thumbnails: terminated = True self.thumbnail_manager.terminate_process( self.generating_thumbnails[scan_pid]) del self.generating_thumbnails[scan_pid] - + if len(self.generating_thumbnails) == 0: self._reset_thumbnail_tracking_and_display() else: terminated = False - + return terminated - + def mark_thumbnails_needed(self, rpd_files): for rpd_file in rpd_files: if rpd_file.unique_id not in self.thumbnails: rpd_file.generate_thumbnail = True - + def _reset_thumbnail_tracking_and_display(self): self.rapid_app.download_progressbar.set_fraction(0.0) self.rapid_app.download_progressbar.set_text('') self.thumbnails_generated = 0 self.total_thumbs_to_generate = 0 - + def thumbnail_results(self, source, condition): connection = self.thumbnail_manager.get_pipe(source) - + conn_type, data = connection.recv() - + if conn_type == rpdmp.CONN_COMPLETE: scan_pid = data del self.generating_thumbnails[scan_pid] connection.close() return False else: - + for thumbnail_data in data: self.update_thumbnail(thumbnail_data) - + self.thumbnails_generated += len(data) - + # clear progress bar information if all thumbnails have been # extracted if self.thumbnails_generated == self.total_thumbs_to_generate: @@ -1006,10 +1011,10 @@ class ThumbnailDisplay(gtk.IconView): if self.total_thumbs_to_generate: self.rapid_app.download_progressbar.set_fraction( float(self.thumbnails_generated) / self.total_thumbs_to_generate) - - + + return True - + def preview_results(self, unique_id, preview_full_size, preview_small): """ Receive a full size preview image and update @@ -1019,24 +1024,24 @@ class ThumbnailDisplay(gtk.IconView): preview_image = preview_full_size.get_image() self.previews[unique_id] = preview_image 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 and preview_small is not None: self._set_thumbnail(unique_id, preview_small.get_image()) - - + + def clear_all(self, scan_pid=None, keep_downloaded_files=False): """ Removes files from display and internal tracking. - + If scan_pid is not None, then only files matching that scan_pid will be removed. Otherwise, everything will be removed. - + If keep_downloaded_files is True, files will not be removed if they have been downloaded. """ if scan_pid is None and not keep_downloaded_files: - + # Here it is critically important to create a brand new liststore, # because the old one is set to be sorted, which is extremely slow. logger.debug("Creating new thumbnails model") @@ -1045,7 +1050,7 @@ class ThumbnailDisplay(gtk.IconView): self.treerow_index = {} self.process_index = {} - + self.rpd_files = {} else: if scan_pid in self.process_index: @@ -1060,80 +1065,80 @@ class ThumbnailDisplay(gtk.IconView): del self.rpd_files[rpd_file.unique_id] if not keep_downloaded_files or not len(self.process_index[scan_pid]): del self.process_index[scan_pid] - + def display_thumbnails(self): self.set_model(self.liststore) - + class TaskManager: def __init__(self, results_callback, batch_size): self.results_callback = results_callback - + # List of actual process, it's terminate_queue, and it's run_event self._processes = [] - + self._pipes = {} self.batch_size = batch_size - + self.paused = False self.no_tasks = 0 - - + + def add_task(self, task): pid = self._setup_task(task) logger.debug("TaskManager PID: %s", pid) self.no_tasks += 1 return pid - + def _setup_task(self, task): task_results_conn, task_process_conn = self._setup_pipe() - + source = task_results_conn.fileno() self._pipes[source] = task_results_conn gobject.io_add_watch(source, gobject.IO_IN, self.results_callback) - + terminate_queue = Queue() run_event = Event() run_event.set() - - return self._initiate_task(task, task_results_conn, task_process_conn, + + return self._initiate_task(task, task_results_conn, task_process_conn, terminate_queue, run_event) - + def _setup_pipe(self): return Pipe(duplex=False) - + def _initiate_task(self, task, task_process_conn, terminate_queue, run_event): logger.error("Implement child class method!") - - + + def processes(self): for i in range(len(self._processes)): yield self._processes[i] - + def start(self): self.paused = False for scan in self.processes(): run_event = scan[2] if not run_event.is_set(): run_event.set() - + def pause(self): self.paused = True for scan in self.processes(): run_event = scan[2] if run_event.is_set(): run_event.clear() - + def _terminate_process(self, p): self._send_termination_msg(p) # The process might be paused: let it run run_event = p[2] if not run_event.is_set(): run_event.set() - + def _send_termination_msg(self, p): p[1].put(None) - + def terminate_process(self, process_id): """ Send a signal to process with matching process_id that it should @@ -1143,7 +1148,7 @@ class TaskManager: if p[0].pid == process_id: if p[0].is_alive(): self._terminate_process(p) - + def request_termination(self): """ Send a signal to processes that they should immediately terminate @@ -1153,29 +1158,29 @@ class TaskManager: if p[0].is_alive(): requested = True self._terminate_process(p) - + return requested - + def terminate_forcefully(self): """ Forcefully terminates any running processes. Use with great caution. - No cleanup action is performed. - + No cleanup action is performed. + As python essential reference (4th edition) says, if the process 'holds a lock or is involved with interprocess communication, terminating it might cause a deadlock or corrupted I/O.' """ - + for p in self.processes(): if p[0].is_alive(): - logger.info("Forcefully terminating %s in %s" , p[0].name, + logger.info("Forcefully terminating %s in %s" , p[0].name, self.__class__.__name__) p[0].terminate() - + def get_pipe(self, source): return self._pipes[source] - + def get_no_active_processes(self): """ Returns how many processes are currently active, i.e. running @@ -1188,38 +1193,38 @@ class TaskManager: class ScanManager(TaskManager): - - def __init__(self, results_callback, batch_size, + + def __init__(self, results_callback, batch_size, add_device_function): TaskManager.__init__(self, results_callback, batch_size) self.add_device_function = add_device_function - - def _initiate_task(self, task, task_results_conn, task_process_conn, + + def _initiate_task(self, task, task_results_conn, task_process_conn, terminate_queue, run_event): - + 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, + self.batch_size, task_process_conn, terminate_queue, run_event) scan.start() self._processes.append((scan, terminate_queue, run_event)) - self.add_device_function(scan.pid, device, + self.add_device_function(scan.pid, device, # This refers to when a device like a hard drive is having its contents scanned, - # looking for photos or videos. It is visible initially in the progress bar for each device + # looking for photos or videos. It is visible initially in the progress bar for each device # (which normally holds "x photos and videos"). # It maybe displayed only briefly if the contents of the device being scanned is small. progress_bar_text=_('scanning...')) - + return scan.pid - + class CopyFilesManager(TaskManager): - - def _initiate_task(self, task, task_results_conn, + + def _initiate_task(self, task, task_results_conn, task_process_conn, terminate_queue, run_event): photo_download_folder = task[0] video_download_folder = task[1] @@ -1227,25 +1232,25 @@ class CopyFilesManager(TaskManager): files = task[3] modify_files_during_download = task[4] modify_pipe = task[5] - + copy_files = copyfiles.CopyFiles(photo_download_folder, video_download_folder, - files, + files, modify_files_during_download, modify_pipe, - scan_pid, self.batch_size, + scan_pid, self.batch_size, task_process_conn, terminate_queue, run_event) copy_files.start() self._processes.append((copy_files, terminate_queue, run_event)) return copy_files.pid - + class ThumbnailManager(TaskManager): def _initiate_task(self, task, task_results_conn, task_process_conn, terminate_queue, run_event): scan_pid = task[0] files = task[1] - generator = tn.GenerateThumbnails(scan_pid, files, self.batch_size, - task_process_conn, terminate_queue, + generator = tn.GenerateThumbnails(scan_pid, files, self.batch_size, + task_process_conn, terminate_queue, run_event) generator.start() self._processes.append((generator, terminate_queue, run_event)) @@ -1256,44 +1261,44 @@ class FileModifyManager(TaskManager): Duplex, multiprocess, similar to BackupFilesManager """ def __init__(self, results_callback): - TaskManager.__init__(self, results_callback=results_callback, + TaskManager.__init__(self, results_callback=results_callback, batch_size=0) self.file_modify_by_scan_pid = {} - - def _initiate_task(self, task, task_results_conn, task_process_conn, + + def _initiate_task(self, task, task_results_conn, task_process_conn, terminate_queue, run_event): scan_pid = task[0] auto_rotate_jpeg = task[1] focal_length = task[2] - + file_modify = filemodify.FileModify(auto_rotate_jpeg, focal_length, - task_process_conn, terminate_queue, + task_process_conn, terminate_queue, run_event) file_modify.start() - self._processes.append((file_modify, terminate_queue, run_event, + self._processes.append((file_modify, terminate_queue, run_event, task_results_conn)) - + self.file_modify_by_scan_pid[scan_pid] = (task_results_conn, file_modify.pid) - + return file_modify.pid def _setup_pipe(self): return Pipe(duplex=True) - + def _send_termination_msg(self, p): p[1].put(None) p[3].send((None, None)) - + def get_modify_pipe(self, scan_pid): return self.file_modify_by_scan_pid[scan_pid][0] - - + + class BackupFilesManager(TaskManager): """ Handles backup processes. This is a little different from some other Task Manager classes in that its pipe is Duplex, and the work done by it is not pre-assigned when the process is started. - + Duplex, multiprocess. """ def __init__(self, results_callback, batch_size): @@ -1302,30 +1307,31 @@ class BackupFilesManager(TaskManager): def _setup_pipe(self): return Pipe(duplex=True) - + def _send_termination_msg(self, p): p[1].put(None) - p[3].send((None, None, None, None)) - - def _initiate_task(self, task, task_results_conn, task_process_conn, + p[3].send((None, None, None, None, None)) + + def _initiate_task(self, task, task_results_conn, task_process_conn, 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, + backup_files = backupfile.BackupFiles(path, name, self.batch_size, + task_process_conn, terminate_queue, run_event) backup_files.start() - self._processes.append((backup_files, terminate_queue, run_event, + self._processes.append((backup_files, terminate_queue, run_event, task_results_conn)) - + 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): + + def backup_file(self, move_succeeded, rpd_file, path_suffix, + backup_duplicate_overwrite, + download_count): if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: logger.debug("Backing up photo %s", rpd_file.download_name) @@ -1334,124 +1340,124 @@ class BackupFilesManager(TaskManager): for path in self.backup_devices_by_path: backup_type = self.backup_devices_by_path[path][2] - if ((backup_type == PHOTO_VIDEO_BACKUP) or + 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)) + task_results_conn.send((move_succeeded, rpd_file, path_suffix, + backup_duplicate_overwrite, download_count)) else: logger.debug("Not backing up to %s", path) - + def add_device(self, path, name, backup_type): """ Convenience function to setup adding a backup device """ return self.add_task((path, name, backup_type)) - + def remove_device(self, path): pid = self.backup_devices_by_path[path][1] self.terminate_process(pid) del self.backup_devices_by_path[path] - - + + class SingleInstanceTaskManager: """ Base class to manage single instance processes. Examples are daemon processes, but also a non-daemon process that has one simple task. - + Core (infrastructure) functionality is implemented in this class. Derived classes should implemented functionality to actually implement specific tasks. """ - def __init__(self, results_callback): + def __init__(self, results_callback): self.results_callback = results_callback - + self.task_results_conn, self.task_process_conn = Pipe(duplex=True) - + source = self.task_results_conn.fileno() gobject.io_add_watch(source, gobject.IO_IN, self.task_results) - + class PreviewManager(SingleInstanceTaskManager): def __init__(self, results_callback): SingleInstanceTaskManager.__init__(self, results_callback) self._get_preview = tn.GetPreviewImage(self.task_process_conn) self._get_preview.start() - + def get_preview(self, unique_id, full_file_name, thm_file_name, file_type, size_max): self.task_results_conn.send((unique_id, full_file_name, thm_file_name, file_type, size_max)) - + def task_results(self, source, condition): unique_id, preview_full_size, preview_small = self.task_results_conn.recv() self.results_callback(unique_id, preview_full_size, preview_small) - return True - + return True + class SubfolderFileManager(SingleInstanceTaskManager): """ Manages the daemon process that renames files and creates subfolders """ def __init__(self, results_callback, sequence_values): SingleInstanceTaskManager.__init__(self, results_callback) - self._subfolder_file = subfolderfile.SubfolderFile(self.task_process_conn, + self._subfolder_file = subfolderfile.SubfolderFile(self.task_process_conn, sequence_values) self._subfolder_file.start() logger.debug("SubfolderFile PID: %s", self._subfolder_file.pid) - - def rename_file_and_move_to_subfolder(self, download_succeeded, + + def rename_file_and_move_to_subfolder(self, download_succeeded, download_count, rpd_file): - - self.task_results_conn.send((download_succeeded, download_count, + + logger.debug("Sending file for rename: %s.", download_count) + self.task_results_conn.send((download_succeeded, download_count, rpd_file)) - logger.debug("Download count: %s.", download_count) - + def task_results(self, source, condition): - move_succeeded, rpd_file = self.task_results_conn.recv() - self.results_callback(move_succeeded, rpd_file) + move_succeeded, rpd_file, download_count = self.task_results_conn.recv() + self.results_callback(move_succeeded, rpd_file, download_count) return True - + class ResizblePilImage(gtk.DrawingArea): def __init__(self, bg_color=None): gtk.DrawingArea.__init__(self) self.base_image = None self.bg_color = bg_color self.connect('expose_event', self.expose) - + def set_image(self, image): self.base_image = image - + #set up sizes and ratio used for drawing the derived image self.base_image_w = self.base_image.size[0] self.base_image_h = self.base_image.size[1] self.base_image_aspect = float(self.base_image_w) / self.base_image_h - + self.queue_draw() - + def expose(self, widget, event): cairo_context = self.window.cairo_create() - - x = event.area.x - y = event.area.y + + x = event.area.x + y = event.area.y w = event.area.width h = event.area.height - - #constrain operations to event area + + #constrain operations to event area cairo_context.rectangle(x, y, w, h) cairo_context.clip_preserve() - + #set background color, if needed if self.bg_color: cairo_context.set_source_rgb(*self.bg_color) - cairo_context.fill_preserve() + cairo_context.fill_preserve() if not self.base_image: return False - + frame_aspect = float(w) / h - + if frame_aspect > self.base_image_aspect: # Frame is wider than image height = h @@ -1460,7 +1466,7 @@ class ResizblePilImage(gtk.DrawingArea): # Frame is taller than image width = w height = int(width / self.base_image_aspect) - + #resize image pil_image = self.base_image.copy() if self.base_image_w < width or self.base_image_h < height: @@ -1473,38 +1479,38 @@ class ResizblePilImage(gtk.DrawingArea): #image width and height image_w = pil_image.size[0] image_h = pil_image.size[1] - + #center the image horizontally and vertically #top left and right corners for the image: image_x = x + ((w - image_w) / 2) image_y = y + ((h - image_h) / 2) - + image = create_cairo_image_surface(pil_image, image_w, image_h) cairo_context.set_source_surface(image, image_x, image_y) - cairo_context.paint() + cairo_context.paint() + + return False + - return False - - class PreviewImage: - + def __init__(self, parent_app, builder): #set background color to equivalent of '#444444 - self.preview_image = ResizblePilImage(bg_color=(0.267, 0.267, 0.267)) + self.preview_image = ResizblePilImage(bg_color=(0.267, 0.267, 0.267)) self.preview_image_eventbox = builder.get_object("preview_eventbox") self.preview_image_eventbox.add(self.preview_image) self.preview_image.show() self.download_this_checkbutton = builder.get_object("download_this_checkbutton") self.rapid_app = parent_app - + self.base_preview_image = None # large size image used to scale down from self.current_preview_size = (0,0) self.preview_image_size_limit = (0,0) - + self.unique_id = None - - def set_preview_image(self, unique_id, pil_image, include_checkbutton_visible=None, + + def set_preview_image(self, unique_id, pil_image, include_checkbutton_visible=None, checked=None): """ """ @@ -1516,72 +1522,74 @@ class PreviewImage: if include_checkbutton_visible is not None: self.download_this_checkbutton.props.visible = include_checkbutton_visible - + def update_preview_image(self, unique_id, pil_image): if unique_id == self.unique_id: self.set_preview_image(unique_id, pil_image) - + class RapidApp(dbus.service.Object): """ The main Rapid Photo Downloader application class. - + Contains functionality for main program window, and directs all other processes. """ - + def __init__(self, bus, path, name, taskserver=None, focal_length=None, - auto_detect=None, device_location=None): - + auto_detect=None, device_location=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(auto_detect, device_location) - + # Initialize widgets in the main window, and variables that point to them self._init_widgets() - self._init_pynotify() - + + if use_pynotify: + self._init_pynotify() + # Initialize job code handling self._init_job_code() - + # Remember the window size from the last time the program was run, or # set a default size self._set_window_size() - + # Setup various widgets self._setup_buttons() self._setup_error_icons() self._setup_icons() - + # Show the main window self.rapidapp.show() - + # Check program preferences - don't allow auto start if there is a problem prefs_valid, msg = prefsrapid.check_prefs_for_validity(self.prefs) if not prefs_valid: self.notify_prefs_are_invalid(details=msg) - + # Initialize variables with which to track important downloads results self._init_download_tracking() - + # Set up process managers. # A task such as scanning a device or copying files is handled in its # own process. self._start_process_managers() - + # Setup devices from which to download from and backup to - self.setup_devices(on_startup=True, on_preference_change=False, + self.setup_devices(on_startup=True, on_preference_change=False, block_auto_start=not prefs_valid) - + # Ensure the device collection scrolled window is not too small self._set_device_collection_size() - + def on_rapidapp_destroy(self, widget, data=None): self._terminate_processes(terminate_file_copies = True) @@ -1592,30 +1600,30 @@ class RapidApp(dbus.service.Object): x, y, width, height = self.rapidapp.get_allocation() self.prefs.main_window_size_x = width self.prefs.main_window_size_y = height - + self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker) - + gtk.main_quit() - + def _terminate_processes(self, terminate_file_copies=False): - + if terminate_file_copies: logger.info("Terminating all processes...") - scan_termination_requested = self.scan_manager.request_termination() + scan_termination_requested = self.scan_manager.request_termination() thumbnails_termination_requested = self.thumbnails.thumbnail_manager.request_termination() backup_termination_requested = self.backup_manager.request_termination() file_modify_termination_requested = self.file_modify_manager.request_termination() - + if terminate_file_copies: copy_files_termination_requested = self.copy_files_manager.request_termination() else: copy_files_termination_requested = False - + if (scan_termination_requested or thumbnails_termination_requested or backup_termination_requested or file_modify_termination_requested): time.sleep(1) - if (self.scan_manager.get_no_active_processes() > 0 or + if (self.scan_manager.get_no_active_processes() > 0 or self.thumbnails.thumbnail_manager.get_no_active_processes() > 0 or self.backup_manager.get_no_active_processes() > 0 or self.file_modify_manager.get_no_active_processes() > 0): @@ -1626,14 +1634,14 @@ class RapidApp(dbus.service.Object): self.scan_manager.terminate_forcefully() self.backup_manager.terminate_forcefully() self.file_modify_manager.terminate_forcefully() - + if terminate_file_copies and copy_files_termination_requested: time.sleep(1) self.copy_files_manager.terminate_forcefully() - + if terminate_file_copies: self._clean_all_temp_dirs() - + # # # # Events and tasks related to displaying preview images and thumbnails # # # @@ -1642,61 +1650,61 @@ class RapidApp(dbus.service.Object): value = checkbutton.get_active() self.thumbnails.set_selected(self.preview_image.unique_id, value) self.set_download_action_sensitivity() - + def on_preview_eventbox_button_press_event(self, widget, event): - + if event.type == gtk.gdk._2BUTTON_PRESS and event.button == 1: - self.show_thumbnails() - + self.show_thumbnails() + def on_show_thumbnails_action_activate(self, action): logger.debug("on_show_thumbnails_action_activate") self.show_thumbnails() - + def on_show_image_action_activate(self, action): logger.debug("on_show_image_action_activate") self.thumbnails.show_preview() - + def on_check_all_action_activate(self, action): self.thumbnails.check_all(check_all=True) - + def on_uncheck_all_action_activate(self, action): self.thumbnails.check_all(check_all=False) def on_check_all_photos_action_activate(self, action): - self.thumbnails.check_all(check_all=True, + self.thumbnails.check_all(check_all=True, file_type=rpdfile.FILE_TYPE_PHOTO) - + def on_check_all_videos_action_activate(self, action): - self.thumbnails.check_all(check_all=True, + self.thumbnails.check_all(check_all=True, file_type=rpdfile.FILE_TYPE_VIDEO) - + def on_quit_action_activate(self, action): self.on_rapidapp_destroy(widget=self.rapidapp, data=None) - + def on_refresh_action_activate(self, action): self.thumbnails.clear_all() self.setup_devices(on_startup=False, on_preference_change=False, block_auto_start=True) - + def on_get_help_action_activate(self, action): webbrowser.open("http://www.damonlynch.net/rapid/help.html") - + def on_about_action_activate(self, action): self.about.set_property("name", PROGRAM_NAME) self.about.set_property("version", utilities.human_readable_version( __version__)) self.about.run() self.about.hide() - + def on_report_problem_action_activate(self, action): webbrowser.open("https://bugs.launchpad.net/rapid") - + def on_translate_action_activate(self, action): webbrowser.open("http://www.damonlynch.net/rapid/translate.html") - + def on_donate_action_activate(self, action): webbrowser.open("http://www.damonlynch.net/rapid/donate.html") - + def show_preview_image(self, unique_id, image, include_checkbutton_visible, checked): if self.main_notebook.get_current_page() == 0: # thumbnails logger.debug("Switching to preview image display") @@ -1704,26 +1712,26 @@ class RapidApp(dbus.service.Object): self.preview_image.set_preview_image(unique_id, image, include_checkbutton_visible, checked) self.next_image_action.set_sensitive(True) self.prev_image_action.set_sensitive(True) - + def update_preview_image(self, unique_id, image): self.preview_image.update_preview_image(unique_id, image) - + def show_thumbnails(self): logger.debug("Switching to thumbnails display") self.main_notebook.set_current_page(0) self.thumbnails.select_image(self.preview_image.unique_id) self.next_image_action.set_sensitive(False) self.prev_image_action.set_sensitive(False) - - + + def on_next_image_action_activate(self, action): if self.preview_image.unique_id is not None: self.thumbnails.show_next_image(self.preview_image.unique_id) - + def on_prev_image_action_activate(self, action): - if self.preview_image.unique_id is not None: + if self.preview_image.unique_id is not None: self.thumbnails.show_prev_image(self.preview_image.unique_id) - + def display_scan_thumbnails(self): """ If all the scans are complete, sets the sort order and displays @@ -1737,35 +1745,36 @@ class RapidApp(dbus.service.Object): # # # # Volume management # # # - + def start_volume_monitor(self): if not self.vmonitor: self.vmonitor = gio.volume_monitor_get() self.vmonitor.connect("mount-added", self.on_mount_added) - self.vmonitor.connect("mount-removed", self.on_mount_removed) - - + self.vmonitor.connect("mount-removed", self.on_mount_removed) + + def _backup_device_name(self, path): if self.backup_devices[path][0] is None: name = path else: 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 + Commences the scanning of a device using the preference values for any paths to ignore while scanning """ - return self.scan_manager.add_task([device, + logger.debug("Starting a device scan for device %s", device.get_name()) + return self.scan_manager.add_task([device, self.prefs.ignored_paths, self.prefs.use_re_ignored_paths]) - + def confirm_manual_location(self): """ - Queries the user to ask if they really want to download from locations + Queries the user to ask if they really want to download from locations that could take a very long time to scan. They can choose yes or no. - + Returns True if yes or there was no need to ask the user, False if the user said no. """ @@ -1781,34 +1790,34 @@ class RapidApp(dbus.service.Object): question="<b>" + _("Downloading from %(location)s.") % {'location': l} + "</b>\n\n" + _("Do you really want to download from here? On some systems, scanning this location can take a very long time."), default_to_yes=False, - use_markup=True) + use_markup=True) response = c.run() user_confirmed = response == gtk.RESPONSE_OK c.destroy() if not user_confirmed: return False return True - + def setup_devices(self, on_startup, on_preference_change, block_auto_start): """ - + Setup devices from which to download from and backup to - + Sets up volumes for downloading from and backing up to - - on_startup should be True if the program is still starting, + + on_startup should be True if the program is still starting, i.e. this is being called from the program's initialization. - + on_preference_change should be True if this is being called as the result of a preference being changed - + block_auto_start should be True if automation options to automatically start a download should be ignored - - Removes any image media that are currently not downloaded, - or finished downloading + + Removes any image media that are currently not downloaded, + or finished downloading """ - + if self.using_volume_monitor(): self.start_volume_monitor() @@ -1816,10 +1825,10 @@ class RapidApp(dbus.service.Object): if not self.prefs.device_autodetection: if not self.confirm_manual_location(): return - + mounts = [] self.backup_devices = {} - + if self.using_volume_monitor(): # either using automatically detected backup devices # or download devices @@ -1827,7 +1836,7 @@ class RapidApp(dbus.service.Object): if not mount.is_shadowed(): path = mount.get_root().get_path() if path: - if (path in self.prefs.device_blacklist and + if (path in self.prefs.device_blacklist and self.search_for_PSD()): logger.info("%s ignored", mount.get_name()) else: @@ -1835,17 +1844,17 @@ class RapidApp(dbus.service.Object): is_backup_mount, backup_file_type = self.check_if_backup_mount(path) if is_backup_mount: self.backup_devices[path] = (mount, backup_file_type) - elif (self.prefs.device_autodetection and - (dv.is_DCIM_device(path) or + 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: - # user manually specified the path from which to download + # user manually specified the path from which to download path = self.prefs.device_location if path: logger.info("Using manually specified path %s", path) @@ -1858,35 +1867,47 @@ class RapidApp(dbus.service.Object): if not self.prefs.backup_device_autodetection: self._setup_manual_backup() self._add_backup_devices() - + self.update_no_backup_devices() - + # Display amount of free space in a status bar message self.display_free_space() - + if block_auto_start: self.auto_start_is_on = False else: self.auto_start_is_on = ((not on_preference_change) and - ((self.prefs.auto_download_at_startup and - on_startup) or + ((self.prefs.auto_download_at_startup and + on_startup) or (self.prefs.auto_download_upon_device_insertion and not on_startup))) - + + logger.debug("Working with %s devices", len(mounts)) for m in mounts: path, mount = m device = dv.Device(path=path, mount=mount) - if (self.search_for_PSD() and - path not in self.prefs.device_whitelist): - # prompt user to see if device should be used or not - self.get_use_device(device) - else: - scan_pid = self.start_device_scan(device) - if mount is not None: - self.mounts_by_path[path] = scan_pid + + + if not self._device_already_detected(device): + if (self.search_for_PSD() and + path not in self.prefs.device_whitelist): + # prompt user to see if device should be used or not + self.get_use_device(device) + else: + 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 _device_already_detected(self, device): + path = device.get_path() + if path in self.mounts_by_path: + logger.debug("Ignoring device %s as already have path %s", device.get_name(), path) + return True + else: + return False + def _setup_manual_backup(self): """ Setup backup devices that the user has manually specified. @@ -1894,8 +1915,8 @@ class RapidApp(dbus.service.Object): 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 + # 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: @@ -1908,7 +1929,7 @@ class RapidApp(dbus.service.Object): 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 @@ -1917,20 +1938,28 @@ class RapidApp(dbus.service.Object): 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): + + + def get_use_device(self, device): """ Prompt user whether or not to download from this device """ - + logger.info("Prompting whether to use %s", device.get_name()) + + # On some systems, e.g. Ubuntu 12.10, the GTK/Gnome environment + # unexpectedly results in a device being added twice and not once. + # The hack on the next line ensures the user is not prompted twice + # for the same device. + + self.mounts_by_path[device.get_path()] = "PROMPTING" + d = dv.UseDeviceDialog(self.rapidapp, device, self.got_use_device) - + def got_use_device(self, dialog, user_selected, permanent_choice, device): """ User has chosen whether or not to use a device to download from """ dialog.destroy() - + path = device.get_path() - + if user_selected: if permanent_choice and path not in self.prefs.device_whitelist: # do NOT do a list append operation here without the assignment, @@ -1941,37 +1970,37 @@ class RapidApp(dbus.service.Object): self.prefs.device_whitelist = [path] 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: # do not do a list append operation here without the assignment, or the preferences will not be updated! if len(self.prefs.device_blacklist): self.prefs.device_blacklist = self.prefs.device_blacklist + [path] else: - self.prefs.device_blacklist = [path] - + self.prefs.device_blacklist = [path] + def search_for_PSD(self): """ - Check to see if user preferences are to automatically search for + Check to see if user preferences are to automatically search for Portable Storage Devices or not """ return self.prefs.device_autodetection_psd and self.prefs.device_autodetection def check_if_backup_mount(self, path): """ - Checks to see if backups are enabled and path represents a valid backup + 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) + (False, None) """ if self.prefs.backup_images: if self.prefs.backup_device_autodetection: - # Determine if the auto-detected backup device is + # 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 + # 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) @@ -1991,7 +2020,7 @@ class RapidApp(dbus.service.Object): 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 + # 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: @@ -2013,22 +2042,22 @@ class RapidApp(dbus.service.Object): #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", + 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.download_tracker.set_no_backup_devices(self.no_photo_backup_devices, self.no_video_backup_devices) def refresh_backup_media(self): """ Setup the backup media - - Assumptions: this is being called after the user has changed their + + Assumptions: this is being called after the user has changed their preferences AND download media has already been setup """ - + # terminate any running backup processes self.backup_manager.request_termination() - + self.backup_devices = {} if self.prefs.backup_images: if not self.prefs.backup_device_autodetection: @@ -2043,22 +2072,22 @@ class RapidApp(dbus.service.Object): # is a backup volume if path not in self.backup_devices: self.backup_devices[path] = (mount, backup_file_type) - + self._add_backup_devices() self.update_no_backup_devices() self.display_free_space() - + def using_volume_monitor(self): """ Returns True if programs needs to use gio volume monitor """ - - return (self.prefs.device_autodetection or - (self.prefs.backup_images and + + return (self.prefs.device_autodetection or + (self.prefs.backup_images and self.prefs.backup_device_autodetection )) - + def on_mount_added(self, vmonitor, mount): """ callback run when gio indicates a new volume @@ -2069,7 +2098,7 @@ class RapidApp(dbus.service.Object): if mount.is_shadowed(): # ignore this type of mount return - + path = mount.get_root().get_path() if path is not None: @@ -2078,7 +2107,7 @@ class RapidApp(dbus.service.Object): 'device': mount.get_name(), 'path': path}) else: 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 @@ -2087,84 +2116,86 @@ class RapidApp(dbus.service.Object): self.update_no_backup_devices() self.display_free_space() - elif self.prefs.device_autodetection and (dv.is_DCIM_device(path) or + elif self.prefs.device_autodetection and (dv.is_DCIM_device(path) or self.search_for_PSD()): - + self.auto_start_is_on = self.prefs.auto_download_upon_device_insertion device = dv.Device(path=path, mount=mount) - if self.search_for_PSD() and path not in self.prefs.device_whitelist: - # prompt user if device should be used or not - self.get_use_device(device) - else: - scan_pid = self.start_device_scan(device) - self.mounts_by_path[path] = scan_pid - + + if not self._device_already_detected(device): + if self.search_for_PSD() and path not in self.prefs.device_whitelist: + # prompt user if device should be used or not + self.get_use_device(device) + else: + scan_pid = self.start_device_scan(device) + self.mounts_by_path[path] = scan_pid + def on_mount_removed(self, vmonitor, mount): """ callback run when gio indicates a new volume has been mounted """ - + path = mount.get_root().get_path() # three scenarios - # the mount has been scanned but downloading has not yet started # files are being downloaded from mount (it must be a messy unmount) # files have finished downloading from mount - + if path in self.mounts_by_path: scan_pid = self.mounts_by_path[path] del self.mounts_by_path[path] # temp directory should be cleaned by finishing of process - - self.thumbnails.clear_all(scan_pid = scan_pid, + + self.thumbnails.clear_all(scan_pid = scan_pid, keep_downloaded_files = True) self.device_collection.remove_device(scan_pid) - - - + + + # remove backup volumes elif path in self.backup_devices: del self.backup_devices[path] self.display_free_space() self.backup_manager.remove_device(path) self.update_no_backup_devices() - + # may need to disable download button and menu self.set_download_action_sensitivity() - + def clear_non_running_downloads(self): """ Clears the display of downloads that are currently not running """ - + # Stop any processes currently scanning or creating thumbnails self._terminate_processes(terminate_file_copies=False) - + # Remove them from the user interface for scan_pid in self.device_collection.get_all_displayed_processes(): if scan_pid not in self.download_active_by_scan_pid: self.device_collection.remove_device(scan_pid) self.thumbnails.clear_all(scan_pid=scan_pid) - - - + + + # # # # Download and help buttons, and menu items # # # - + def on_download_action_activate(self, action): """ Called when a download is activated """ - + if self.copy_files_manager.paused: logger.debug("Download resumed") self.resume_download() else: logger.debug("Download activated") - + if self.download_action_is_download: if self.need_job_code_for_naming and not self.prompting_for_job_code: self.get_job_code() @@ -2173,18 +2204,18 @@ class RapidApp(dbus.service.Object): else: self.pause_download() - + def on_help_action_activate(self, action): webbrowser.open("http://www.damonlynch.net/rapid/documentation") - + def on_preferences_action_activate(self, action): preferencesdialog.PreferencesDialog(self) - + def set_download_action_sensitivity(self): """ Sets sensitivity of Download action to enable or disable it - + Affects download button and menu item """ if not self.download_is_occurring(): @@ -2192,26 +2223,26 @@ class RapidApp(dbus.service.Object): if self.scan_manager.no_tasks == 0: if self.thumbnails.files_are_checked_to_download(): sensitivity = True - + self.download_action.set_sensitive(sensitivity) - + def set_download_action_label(self, is_download): """ - Toggles label betwen pause and download + Toggles label betwen pause and download """ - + if is_download: self.download_action.set_label(_("Download")) self.download_action_is_download = True else: self.download_action.set_label(_("Pause")) self.download_action_is_download = False - + # # # # Job codes # # # - - + + def _init_job_code(self): self.job_code = self.last_chosen_job_code = '' self.need_job_code_for_naming = self.prefs.any_pref_uses_job_code() @@ -2219,64 +2250,64 @@ class RapidApp(dbus.service.Object): def assign_job_code(self, code): """ assign job code (which may be empty) to member variable and update user preferences - + Update preferences only if code is not empty. Do not duplicate job code. """ self.job_code = code - + if code: #add this value to job codes preferences #delete any existing value which is the same #(this way it comes to the front, which is where it should be) #never modify self.prefs.job_codes in place! (or prefs become screwed up) - + jcs = self.prefs.job_codes while code in jcs: jcs.remove(code) - + self.prefs.job_codes = [code] + jcs def _get_job_code(self, post_job_code_entry_callback): """ prompt for a job code """ - + if not self.prompting_for_job_code: logger.debug("Prompting for Job Code") self.prompting_for_job_code = True j = preferencesdialog.JobCodeDialog(parent_window = self.rapidapp, job_codes = self.prefs.job_codes, - default_job_code = self.last_chosen_job_code, + default_job_code = self.last_chosen_job_code, post_job_code_entry_callback=post_job_code_entry_callback, entry_only = False) else: logger.debug("Already prompting for Job Code, do not prompt again") - + def get_job_code(self): self._get_job_code(self.got_job_code) - + def got_job_code(self, dialog, user_chose_code, code): dialog.destroy() self.prompting_for_job_code = False - + if user_chose_code: if code is None: code = '' self.assign_job_code(code) self.last_chosen_job_code = code logger.debug("Job Code %s entered", self.job_code) - self.start_download() - + self.start_download() + else: # user cancelled logger.debug("No Job Code entered") self.job_code = '' self.auto_start_is_on = False - - + + # # # # Download # # # - + def _init_download_tracking(self): """ Initialize variables to track downloads @@ -2285,28 +2316,28 @@ class RapidApp(dbus.service.Object): # (Scan id acts as an index to each device. A device could be scanned # more than once). self.download_tracker = downloadtracker.DownloadTracker() - + # Track which temporary directories are created when downloading files self.temp_dirs_by_scan_pid = dict() - + # Track which downloads are running self.download_active_by_scan_pid = [] - + def modify_files_during_download(self): """ Returns True if there is a need to modify files during download""" return self.prefs.auto_rotate_jpeg or (self.focal_length is not None) - + def start_download(self, scan_pid=None): """ Start download, renaming and backup of files. - + If scan_pid is specified, only files matching it will be downloaded """ - + files_by_scan_pid = self.thumbnails.get_files_checked_for_download(scan_pid) folders_valid, invalid_dirs = self.check_download_folder_validity(files_by_scan_pid) - + if not folders_valid: if len(invalid_dirs) > 1: msg = _("These download folders are invalid:\n%(folder1)s\n%(folder2)s") % { @@ -2319,40 +2350,40 @@ class RapidApp(dbus.service.Object): # set time download is starting if it is not already set # it is unset when all downloads are completed if self.download_start_time is None: - self.download_start_time = datetime.datetime.now() + self.download_start_time = datetime.datetime.now() - # Set status to download pending + # Set status to download pending self.thumbnails.mark_download_pending(files_by_scan_pid) - + # disable refresh and preferences change while download is occurring self.enable_prefs_and_refresh(enabled=False) - + for scan_pid in files_by_scan_pid: files = files_by_scan_pid[scan_pid] # if generating thumbnails for this scan_pid, stop it if self.thumbnails.terminate_thumbnail_generation(scan_pid): self.thumbnails.mark_thumbnails_needed(files) - + self.download_files(files, scan_pid) - + self.set_download_action_label(is_download = False) - + def pause_download(self): - + self.copy_files_manager.pause() - + # set action to display Download if not self.download_action_is_download: self.set_download_action_label(is_download = True) - + self.time_check.pause() - + def resume_download(self): for scan_pid in self.download_active_by_scan_pid: self.time_remaining.set_time_mark(scan_pid) - + self.time_check.set_download_mark() - + self.copy_files_manager.start() def download_files(self, files, scan_pid): @@ -2360,16 +2391,16 @@ class RapidApp(dbus.service.Object): Initiate downloading and renaming of files """ # Check which file types will be downloaded for this particular process - no_photos_to_download = 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 DOWNLOAD_VIDEO: - no_videos_to_download = self.files_of_type_present(files, + no_videos_to_download = self.files_of_type_present(files, rpdfile.FILE_TYPE_VIDEO, return_file_count=True) if no_videos_to_download: @@ -2379,30 +2410,30 @@ class RapidApp(dbus.service.Object): else: video_download_folder = None no_videos_to_download = 0 - + photo_download_size, video_download_size = self.size_files_to_be_downloaded(files) - self.download_tracker.init_stats(scan_pid=scan_pid, - photo_size_in_bytes=photo_download_size, + self.download_tracker.init_stats(scan_pid=scan_pid, + 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 + ((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() - + self.download_active_by_scan_pid.append(scan_pid) - - + + if len(self.download_active_by_scan_pid) > 1: self.display_summary_notification = True - + if self.auto_start_is_on and self.prefs.generate_thumbnails: for rpd_file in files: rpd_file.generate_thumbnail = True @@ -2414,13 +2445,13 @@ class RapidApp(dbus.service.Object): else: modify_pipe = None - + # Initiate copy files process - self.copy_files_manager.add_task((photo_download_folder, + self.copy_files_manager.add_task((photo_download_folder, video_download_folder, scan_pid, files, modify_files_during_download, modify_pipe)) - + def copy_files_results(self, source, condition): """ Handle results from copy files process @@ -2434,7 +2465,7 @@ class RapidApp(dbus.service.Object): if msg_type == rpdmp.MSG_TEMP_DIRS: scan_pid, photo_temp_dir, video_temp_dir = data self.temp_dirs_by_scan_pid[scan_pid] = (photo_temp_dir, video_temp_dir) - + # Report which temporary directories are being used for this # download if photo_temp_dir and video_temp_dir: @@ -2445,10 +2476,10 @@ class RapidApp(dbus.service.Object): photo_temp_dir) else: logger.debug("Using temp dir %s (videos)", - video_temp_dir) + video_temp_dir) elif msg_type == rpdmp.MSG_BYTES: scan_pid, total_downloaded, chunk_downloaded = data - self.download_tracker.set_total_bytes_copied(scan_pid, + self.download_tracker.set_total_bytes_copied(scan_pid, total_downloaded) self.time_check.increment(bytes_downloaded=chunk_downloaded) percent_complete = self.download_tracker.get_percent_complete(scan_pid) @@ -2457,39 +2488,39 @@ class RapidApp(dbus.service.Object): self.time_remaining.update(scan_pid, bytes_downloaded=chunk_downloaded) elif msg_type == rpdmp.MSG_FILE: self.copy_file_results_single_file(data) - + return True else: # Process is complete, i.e. conn_type == rpdmp.CONN_COMPLETE connection.close() return False - + def copy_file_results_single_file(self, data): """ Handles results from one of two processes: 1. copy_files 2. file_modify - + Operates after a single file has been copied from the download device to the local folder. - + Calls the process to rename files and create subfolders (subfolderfile) """ - + download_succeeded, rpd_file, download_count, temp_full_file_name, thumbnail_icon, thumbnail = data - + if thumbnail is not None or thumbnail_icon is not None: - self.thumbnails.update_thumbnail((rpd_file.unique_id, - thumbnail_icon, + self.thumbnails.update_thumbnail((rpd_file.unique_id, + thumbnail_icon, thumbnail)) - + self.download_tracker.set_download_count_for_file( rpd_file.unique_id, download_count) self.download_tracker.set_download_count( rpd_file.scan_pid, download_count) rpd_file.download_start_time = self.download_start_time - + if download_succeeded: # Insert preference values needed for name generation rpd_file = prefsrapid.insert_pref_lists(self.prefs, rpd_file) @@ -2498,21 +2529,21 @@ class RapidApp(dbus.service.Object): rpd_file.download_conflict_resolution = self.prefs.download_conflict_resolution rpd_file.synchronize_raw_jpg = self.prefs.must_synchronize_raw_jpg() rpd_file.job_code = self.job_code - + self.subfolder_file_manager.rename_file_and_move_to_subfolder( - download_succeeded, - download_count, + download_succeeded, + download_count, rpd_file - ) + ) def file_modify_results(self, source, condition): """ - 'file modify' is a process that runs immediately after 'copy files', - meaning there can be more than one at one time. - + 'file modify' is a process that runs immediately after 'copy files', + meaning there can be more than one at one time. + It runs before the renaming process. """ connection = self.file_modify_manager.get_pipe(source) - + conn_type, data = connection.recv() if conn_type == rpdmp.CONN_PARTIAL: self.copy_file_results_single_file(data) @@ -2520,31 +2551,31 @@ class RapidApp(dbus.service.Object): else: # Process is complete, i.e. conn_type == rpdmp.CONN_COMPLETE connection.close() - return False + return False + - def download_is_occurring(self): - """Returns True if a file is currently being downloaded, renamed or + """Returns True if a file is currently being downloaded, renamed or backed up """ return not len(self.download_active_by_scan_pid) == 0 - + # # # # Create folder and file names for downloaded files # # # - - def subfolder_file_results(self, move_succeeded, rpd_file): + + def subfolder_file_results(self, move_succeeded, rpd_file, download_count): """ Handle results of subfolder creation and file renaming """ - + scan_pid = rpd_file.scan_pid unique_id = rpd_file.unique_id - + if rpd_file.status == config.STATUS_DOWNLOADED_WITH_WARNING: - self.log_error(config.WARNING, rpd_file.error_title, + self.log_error(config.WARNING, rpd_file.error_title, rpd_file.error_msg, rpd_file.error_extra_detail) - + if self.prefs.backup_images and len(self.backup_devices): if self.prefs.backup_device_autodetection: if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: @@ -2553,23 +2584,24 @@ class RapidApp(dbus.service.Object): path_suffix = self.prefs.video_backup_identifier else: path_suffix = None - - self.backup_manager.backup_file(move_succeeded, rpd_file, + + self.backup_manager.backup_file(move_succeeded, rpd_file, path_suffix, - self.prefs.backup_duplicate_overwrite) + self.prefs.backup_duplicate_overwrite, + download_count) 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 + return ((file_type == rpdfile.FILE_TYPE_PHOTO and self.no_photo_backup_devices > 1) or - (file_type == rpdfile.FILE_TYPE_VIDEO and + (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 @@ -2578,30 +2610,30 @@ class RapidApp(dbus.service.Object): conn_type, msg_data = connection.recv() if conn_type == rpdmp.CONN_PARTIAL: msg_type, data = msg_data - + if msg_type == rpdmp.MSG_BYTES: scan_pid, backup_pid, total_downloaded, chunk_downloaded = data - self.download_tracker.increment_bytes_backed_up(scan_pid, + self.download_tracker.increment_bytes_backed_up(scan_pid, chunk_downloaded) self.time_check.increment(bytes_downloaded=chunk_downloaded) percent_complete = self.download_tracker.get_percent_complete(scan_pid) self.device_collection.update_progress(scan_pid, percent_complete, None, None) self.time_remaining.update(scan_pid, bytes_downloaded=chunk_downloaded) - + 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 + # 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, + 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, rpd_file.file_type): @@ -2610,7 +2642,7 @@ class RapidApp(dbus.service.Object): else: return False - + def file_download_finished(self, succeeded, rpd_file): """ Called when a file has been downloaded i.e. copied, renamed, and backed up @@ -2619,23 +2651,23 @@ class RapidApp(dbus.service.Object): unique_id = rpd_file.unique_id # Update error log window if neccessary if not succeeded and not self.multiple_backup_devices(rpd_file.file_type): - self.log_error(config.SERIOUS_ERROR, rpd_file.error_title, + self.log_error(config.SERIOUS_ERROR, rpd_file.error_title, rpd_file.error_msg, rpd_file.error_extra_detail) elif self.prefs.auto_delete: - # record which files to automatically delete when download + # record which files to automatically delete when download # completes self.download_tracker.add_to_auto_delete(rpd_file) - + self.thumbnails.update_status_post_download(rpd_file) - self.download_tracker.file_downloaded_increment(scan_pid, + self.download_tracker.file_downloaded_increment(scan_pid, rpd_file.file_type, rpd_file.status) - + 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() - + if completed: # Last file for this scan pid has been downloaded, so clean temp directory logger.debug("Purging temp directories") @@ -2649,63 +2681,63 @@ class RapidApp(dbus.service.Object): self.notify_downloaded_from_device(scan_pid) if files_remaining == 0 and self.prefs.auto_unmount: self.device_collection.unmount(scan_pid) - - + + if not self.download_is_occurring(): logger.debug("Download completed") self.enable_prefs_and_refresh(enabled=True) self.notify_download_complete() self.download_progressbar.set_fraction(0.0) - + self.prefs.stored_sequence_no = self.stored_sequence_value.value self.downloads_today_tracker.set_raw_downloads_today_from_int(self.downloads_today_value.value) self.downloads_today_tracker.set_raw_downloads_today_date(self.downloads_today_date_value.value) self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker) - if ((self.prefs.auto_exit and self.download_tracker.no_errors_or_warnings()) + if ((self.prefs.auto_exit and self.download_tracker.no_errors_or_warnings()) or self.prefs.auto_exit_force): if not self.thumbnails.files_remain_to_download(): self._terminate_processes() gtk.main_quit() - + self.download_tracker.purge_all() self.speed_label.set_label(" ") - + self.display_free_space() - + self.set_download_action_label(is_download=True) self.set_download_action_sensitivity() - + self.job_code = '' self.download_start_time = None - - + + def update_time_remaining(self): update, download_speed = self.time_check.check_for_update() if update: self.speed_label.set_text(download_speed) - + time_remaining = self.time_remaining.time_remaining() if time_remaining: secs = int(time_remaining) - + if secs == 0: message = "" elif secs == 1: message = _("About 1 second remaining") elif secs < 60: - message = _("About %i seconds remaining") % secs + message = _("About %i seconds remaining") % secs elif secs == 60: message = _("About 1 minute remaining") else: - # Translators: in the text '%(minutes)i:%(seconds)02i', only the : should be translated, if needed. + # Translators: in the text '%(minutes)i:%(seconds)02i', only the : should be translated, if needed. # '%(minutes)i' and '%(seconds)02i' should not be modified or left out. They are used to format and display the amount # of time the download has remainging, e.g. 'About 5:36 minutes remaining' message = _("About %(minutes)i:%(seconds)02i minutes remaining") % {'minutes': secs / 60, 'seconds': secs % 60} - + self.rapid_statusbar.pop(self.statusbar_context_id) - self.rapid_statusbar.push(self.statusbar_context_id, message) - + self.rapid_statusbar.push(self.statusbar_context_id, message) + def auto_delete(self, scan_pid): """Delete files from download device at completion of download""" for file in self.download_tracker.get_files_to_auto_delete(scan_pid): @@ -2714,10 +2746,10 @@ class RapidApp(dbus.service.Object): f.delete(cancellable=None) except gio.Error, inst: logger.error("Failure deleting file %s", file) - logger.error(inst) - + logger.error(inst) + def file_types_by_number(self, no_photos, no_videos): - """ + """ returns a string to be displayed to the user that can be used to show if a value refers to photos or videos or both, or just one of each @@ -2740,14 +2772,14 @@ class RapidApp(dbus.service.Object): def notify_downloaded_from_device(self, scan_pid): device = self.device_collection.get_device(scan_pid) - + if device.mount is None: notification_name = PROGRAM_NAME icon = self.application_icon else: notification_name = device.get_name() icon = device.get_icon(self.notification_icon_size) - + no_photos_downloaded = self.download_tracker.get_no_files_downloaded( scan_pid, rpdfile.FILE_TYPE_PHOTO) no_videos_downloaded = self.download_tracker.get_no_files_downloaded( @@ -2759,36 +2791,37 @@ class RapidApp(dbus.service.Object): no_files_downloaded = no_photos_downloaded + no_videos_downloaded no_files_failed = no_photos_failed + no_videos_failed no_warnings = self.download_tracker.get_no_warnings(scan_pid) - + file_types = self.file_types_by_number(no_photos_downloaded, no_videos_downloaded) file_types_failed = self.file_types_by_number(no_photos_failed, no_videos_failed) message = _("%(noFiles)s %(filetypes)s downloaded") % \ {'noFiles':no_files_downloaded, 'filetypes': file_types} - + if no_files_failed: message += "\n" + _("%(noFiles)s %(filetypes)s failed to download") % {'noFiles':no_files_failed, 'filetypes':file_types_failed} - + if no_warnings: - message = "%s\n%s " % (message, no_warnings) + _("warnings") - - n = pynotify.Notification(notification_name, message) - n.set_icon_from_pixbuf(icon) - - n.show() - + message = "%s\n%s " % (message, no_warnings) + _("warnings") + + if use_pynotify: + n = pynotify.Notification(notification_name, message) + n.set_icon_from_pixbuf(icon) + + n.show() + def notify_download_complete(self): if self.display_summary_notification: message = _("All downloads complete") - + # photo downloads photo_downloads = self.download_tracker.total_photos_downloaded if photo_downloads: filetype = self.file_types_by_number(photo_downloads, 0) message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': photo_downloads, + {'number': photo_downloads, 'numberdownloaded': _("%(filetype)s downloaded") % \ {'filetype': filetype}} - + # photo failures photo_failures = self.download_tracker.total_photo_failures if photo_failures: @@ -2797,16 +2830,16 @@ class RapidApp(dbus.service.Object): {'number': photo_failures, 'numberdownloaded': _("%(filetype)s failed to download") % \ {'filetype': filetype}} - + # video downloads video_downloads = self.download_tracker.total_videos_downloaded if video_downloads: filetype = self.file_types_by_number(0, video_downloads) message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': video_downloads, + {'number': video_downloads, 'numberdownloaded': _("%(filetype)s downloaded") % \ {'filetype': filetype}} - + # video failures video_failures = self.download_tracker.total_video_failures if video_failures: @@ -2815,65 +2848,66 @@ class RapidApp(dbus.service.Object): {'number': video_failures, 'numberdownloaded': _("%(filetype)s failed to download") % \ {'filetype': filetype}} - + # warnings - warnings = self.download_tracker.total_warnings + warnings = self.download_tracker.total_warnings if warnings: message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': warnings, + {'number': warnings, 'numberdownloaded': _("warnings")} - - n = pynotify.Notification(PROGRAM_NAME, message) - n.set_icon_from_pixbuf(self.application_icon) - n.show() + + if use_pynotify: + n = pynotify.Notification(PROGRAM_NAME, message) + n.set_icon_from_pixbuf(self.application_icon) + n.show() self.display_summary_notification = False # don't show it again unless needed - - + + def _update_file_download_device_progress(self, scan_pid, unique_id, file_type): """ Increments the progress bar for an individual device - + Returns if the download is completed for that scan_pid It also returns the number of files remaining for the scan_pid, BUT this value is valid ONLY if the download is completed """ - + files_downloaded = self.download_tracker.get_download_count_for_file(unique_id) files_to_download = self.download_tracker.get_no_files_in_download(scan_pid) 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, file_type) - + if completed: files_remaining = self.thumbnails.get_no_files_remaining(scan_pid) else: files_remaining = 0 - + if completed and files_remaining: # e.g.: 3 of 205 photos and videos (202 remaining) progress_bar_text = _("%(number)s of %(total)s %(filetypes)s (%(remaining)s remaining)") % { - 'number': files_downloaded, + 'number': files_downloaded, 'total': files_to_download, 'filetypes': file_types, 'remaining': files_remaining} else: # e.g.: 205 of 205 photos and videos progress_bar_text = _("%(number)s of %(total)s %(filetypes)s") % \ - {'number': files_downloaded, + {'number': files_downloaded, 'total': files_to_download, 'filetypes': file_types} percent_complete = self.download_tracker.get_percent_complete(scan_pid) self.device_collection.update_progress(scan_pid=scan_pid, percent_complete=percent_complete, - progress_bar_text=progress_bar_text, + progress_bar_text=progress_bar_text, bytes_downloaded=None) - + percent_complete = self.download_tracker.get_overall_percent_complete() self.download_progressbar.set_fraction(percent_complete) - + return (completed, files_remaining) - + def _clean_all_temp_dirs(self): """ @@ -2882,10 +2916,10 @@ class RapidApp(dbus.service.Object): for scan_pid in self.temp_dirs_by_scan_pid: for temp_dir in self.temp_dirs_by_scan_pid[scan_pid]: self._purge_dir(temp_dir) - + self.temp_dirs_by_scan_pid = {} - - + + def _clean_temp_dirs_for_scan_pid(self, scan_pid): """ Deletes temp files and folders used in download @@ -2897,10 +2931,10 @@ class RapidApp(dbus.service.Object): def _purge_dir(self, directory): """ Deletes all files in the directory, and the directory itself. - + Does not recursively traverse any subfolders in the directory. """ - + if directory: try: path = gio.File(directory) @@ -2916,16 +2950,16 @@ class RapidApp(dbus.service.Object): except gio.Error, inst: logger.error("Failure deleting temporary folder %s", directory) logger.error(inst) - - - # # # + + + # # # # Preferences # # # - - - def _init_prefs(self, auto_detect, device_location): + + + def _init_prefs(self, auto_detect, device_location): self.prefs = prefsrapid.RapidPreferences() - + # handle device preferences set from the command line # do this before preference changes are handled with notify_add if auto_detect: @@ -2933,82 +2967,82 @@ class RapidApp(dbus.service.Object): elif device_location: self.prefs.device_location = device_location self.prefs.device_autodetection = False - + self.prefs.notify_add(self.on_preference_changed) - - # flag to indicate whether the user changed some preferences that + + # flag to indicate whether the user changed some preferences that # indicate the image and backup devices should be setup again self.rerun_setup_available_image_and_video_media = False self.rerun_setup_available_backup_media = False - - # flag to indicate that the preferences dialog window is being + + # flag to indicate that the preferences dialog window is being # displayed to the user self.preferences_dialog_displayed = False # flag to indicate that the user has modified the download today # related values in the preferences dialog window self.refresh_downloads_today = False - - # these values are used to track the number of backup devices / + + # 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() if downloads_today > 0: logger.info("Downloads that have occurred so far today: %s", downloads_today) else: - logger.info("No downloads have occurred so far today") + logger.info("No downloads have occurred so far today") - self.downloads_today_value = Value(c_int, + self.downloads_today_value = Value(c_int, self.downloads_today_tracker.get_raw_downloads_today()) self.downloads_today_date_value = Array(c_char, self.downloads_today_tracker.get_raw_downloads_today_date()) - self.day_start_value = Array(c_char, + self.day_start_value = Array(c_char, self.downloads_today_tracker.get_raw_day_start()) self.refresh_downloads_today_value = Value(c_bool, False) self.stored_sequence_value = Value(c_int, self.prefs.stored_sequence_no) self.uses_stored_sequence_no_value = Value(c_bool, self.prefs.any_pref_uses_stored_sequence_no()) 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): self.uses_stored_sequence_no_value.value = self.prefs.any_pref_uses_stored_sequence_no() 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() - + 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 + 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 + # 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", + 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 @@ -3017,90 +3051,89 @@ 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', + 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 self._set_from_toolbar_state() if not self.preferences_dialog_displayed: self.post_preference_change() - - - elif key in ['backup_images', 'backup_device_autodetection', - 'backup_location', 'backup_video_location', + + + 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() - + # Downloads today and stored sequence numbers are kept in shared memory, # so that the subfolderfile daemon process can access and modify them - + # Note, totally ignore any changes in downloads today, as it # is modified in a special manner via a tracking class - + elif key == 'stored_sequence_no': if type(value) <> types.IntType: logger.critical("Stored sequence number value is malformed") else: self.stored_sequence_value.value = value - + elif key in ['image_rename', 'subfolder', 'video_rename', 'video_subfolder']: self.need_job_code_for_naming = self.prefs.any_pref_uses_job_code() # Check if stored sequence no is being used self._check_for_sequence_value_use() - + elif key in ['download_folder', 'video_download_folder']: self._set_to_toolbar_values() self.display_free_space() - + def post_preference_change(self): if self.rerun_setup_available_image_and_video_media: logger.info("Download device settings preferences were changed") - + self.thumbnails.clear_all() self.setup_devices(on_startup = False, on_preference_change = True, block_auto_start = True) self._set_device_collection_size() - + if self.main_notebook.get_current_page() == 1: # preview of file self.main_notebook.set_current_page(0) - + self.rerun_setup_available_image_and_video_media = False - + if self.rerun_setup_available_backup_media: if self.using_volume_monitor(): - self.start_volume_monitor() + self.start_volume_monitor() logger.info("Backup preferences were changed.") - + self.refresh_backup_media() - + self.rerun_setup_available_backup_media = False - + if self.refresh_downloads_today: self.downloads_today_value.value = self.downloads_today_tracker.get_raw_downloads_today() self.downloads_today_date_value.value = self.downloads_today_tracker.get_raw_downloads_today_date() self.day_start_value.value = self.downloads_today_tracker.get_raw_day_start() self.refresh_downloads_today_value.value = True self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker) - - + + # # # # Main app window management and setup # # # - + def _init_pynotify(self): """ Initialize system notification messages """ - + if not pynotify.init("TestCaps"): logger.warning("There might be problems using pynotify.") - #~ sys.exit(1) do_not_size_icon = False - self.notification_icon_size = 48 + self.notification_icon_size = 48 try: info = pynotify.get_server_info() except: @@ -3111,7 +3144,7 @@ class RapidApp(dbus.service.Object): do_not_size_icon = True except: pass - + if do_not_size_icon: self.application_icon = gtk.gdk.pixbuf_new_from_file( paths.share_dir('glade3/rapid-photo-downloader.svg')) @@ -3137,7 +3170,7 @@ class RapidApp(dbus.service.Object): self.main_notebook = builder.get_object("main_notebook") self.download_action = builder.get_object("download_action") self.download_button = builder.get_object("download_button") - + self.download_progressbar = builder.get_object("download_progressbar") self.rapid_statusbar = builder.get_object("rapid_statusbar") self.statusbar_context_id = self.rapid_statusbar.get_context_id("progress") @@ -3148,71 +3181,71 @@ class RapidApp(dbus.service.Object): self.speed_label = builder.get_object("speed_label") self.refresh_action = builder.get_object("refresh_action") self.preferences_action = builder.get_object("preferences_action") - + # Only enable this action when actually displaying a preview self.next_image_action.set_sensitive(False) self.prev_image_action.set_sensitive(False) - + self._init_toolbars() - + # About dialog builder.add_from_file(paths.share_dir("glade3/about.ui")) self.about = builder.get_object("about") - + builder.connect_signals(self) - + self.preview_image = PreviewImage(self, builder) thumbnails_scrolledwindow = builder.get_object('thumbnails_scrolledwindow') self.thumbnails = ThumbnailDisplay(self) - thumbnails_scrolledwindow.add(self.thumbnails) - + thumbnails_scrolledwindow.add(self.thumbnails) + #collection of devices from which to download self.device_collection_viewport = builder.get_object("device_collection_viewport") self.device_collection = DeviceCollection(self) self.device_collection_viewport.add(self.device_collection) - + #error log window self.error_log = errorlog.ErrorLog(self) - + # monitor to handle mounts and dismounts self.vmonitor = None # track scan ids for mount paths - very useful when a device is unmounted self.mounts_by_path = {} - + # Download action state self.download_action_is_download = True - + # Track the time a download commences self.download_start_time = None - + # Whether a system wide notifcation message should be shown # after a download has occurred in parallel self.display_summary_notification = False - + # Values used to display how much longer a download will take self.time_remaining = downloadtracker.TimeRemaining() self.time_check = downloadtracker.TimeCheck() - + def _init_toolbars(self): """ Setup the 3 vertical toolbars on the main screen """ self._setup_from_toolbar() self._setup_copy_move_toolbar() self._setup_dest_toolbar() - + # size label widths so they are equal, or else the left border of the file chooser will not match self.photo_dest_label.realize() self._make_widget_widths_equal(self.photo_dest_label, self.video_dest_label) self.photo_dest_label.set_alignment(xalign=0.0, yalign=0.5) self.video_dest_label.set_alignment(xalign=0.0, yalign=0.5) - + # size copy / move buttons so they are equal in length, so arrows align self._make_widget_widths_equal(self.copy_button, self.move_button) - + def _setup_from_toolbar(self): self.from_toolbar.set_style(gtk.TOOLBAR_TEXT) self.from_toolbar.set_border_width(5) - + from_label = gtk.Label() from_label.set_markup("<i>" + _("From") + "</i>") self.from_toolbar_label = gtk.ToolItem() @@ -3229,32 +3262,32 @@ class RapidApp(dbus.service.Object): _("Select a folder containing %(file_types)s") % {'file_types':file_types_to_download()}) self.from_filechooser_button.set_action( gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) - + self.from_filechooser = gtk.ToolItem() self.from_filechooser.set_is_important(True) self.from_filechooser.add(self.from_filechooser_button) self.from_filechooser.set_expand(True) self.from_toolbar.insert(self.from_filechooser, 2) - + self._set_from_toolbar_state() - + #set events after having initialized the values self.auto_detect_button.connect("toggled", self.on_auto_detect_button_toggled_event) - self.from_filechooser_button.connect("selection-changed", + self.from_filechooser_button.connect("selection-changed", self.on_from_filechooser_button_selection_changed) - + self.from_toolbar.show_all() - + def _setup_copy_move_toolbar(self): self.copy_toolbar.set_style(gtk.TOOLBAR_TEXT) self.copy_toolbar.set_border_width(5) - + copy_move_label = gtk.Label(" ") self.copy_move_toolbar_label = gtk.ToolItem() self.copy_move_toolbar_label.add(copy_move_label) self.copy_move_toolbar_label.set_is_important(True) self.copy_toolbar.insert(self.copy_move_toolbar_label, 0) - + self.copy_hbox = gtk.HBox() self.move_hbox = gtk.HBox() self.forward_image = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_SMALL_TOOLBAR) @@ -3265,7 +3298,7 @@ class RapidApp(dbus.service.Object): self.forward_label2 = gtk.Label(" ") self.forward_label3 = gtk.Label(" ") self.forward_label4 = gtk.Label(" ") - + self.copy_button = gtk.RadioToolButton() self.copy_button.set_label(_("Copy")) self.copy_button.set_is_important(True) @@ -3278,7 +3311,7 @@ class RapidApp(dbus.service.Object): copy_box = gtk.ToolItem() copy_box.add(self.copy_hbox) self.copy_toolbar.insert(copy_box, 1) - + self.move_button = gtk.RadioToolButton(self.copy_button) self.move_button.set_label(_("Move")) self.move_button.set_is_important(True) @@ -3290,27 +3323,27 @@ class RapidApp(dbus.service.Object): move_box = gtk.ToolItem() move_box.add(self.move_hbox) self.copy_toolbar.insert(move_box, 2) - + self.move_button.set_active(self.prefs.auto_delete) - self.copy_button.connect("toggled", self.on_copy_button_toggle_event) - + self.copy_button.connect("toggled", self.on_copy_button_toggle_event) + self.copy_toolbar.show_all() - self._set_copy_toolbar_active_arrows() - + self._set_copy_toolbar_active_arrows() + def _setup_dest_toolbar(self): #Destination Toolbar self.dest_toolbar.set_border_width(5) - + dest_label = gtk.Label() dest_label.set_markup("<i>" + _("To") + "</i>") self.dest_toolbar_label = gtk.ToolItem() self.dest_toolbar_label.add(dest_label) self.dest_toolbar_label.set_is_important(True) self.dest_toolbar.insert(self.dest_toolbar_label, 0) - + photo_dest_hbox = gtk.HBox() self.photo_dest_label = gtk.Label(_("Photos:")) - + self.to_photo_filechooser_button = gtk.FileChooserButton( _("Select a folder to download photos to")) self.to_photo_filechooser_button.set_action( @@ -3336,9 +3369,9 @@ class RapidApp(dbus.service.Object): self.to_video_filechooser.set_expand(True) self.to_video_filechooser.add(video_dest_hbox) self.dest_toolbar.insert(self.to_video_filechooser, 2) - + self._set_to_toolbar_values() - self.to_photo_filechooser_button.connect("selection-changed", + self.to_photo_filechooser_button.connect("selection-changed", self.on_to_photo_filechooser_button_selection_changed) self.to_video_filechooser_button.connect("selection-changed", self.on_to_video_filechooser_button_selection_changed) @@ -3346,14 +3379,14 @@ class RapidApp(dbus.service.Object): def _make_widget_widths_equal(self, widget1, widget2): """takes two widgets and sets a width for both equal to widest one""" - + x1, y1, w1, h1 = widget1.get_allocation() x2, y2, w2, h2 = widget2.get_allocation() w = max(w1, w2) h = max(h1, h2) widget1.set_size_request(w,h) widget2.set_size_request(w,h) - + def _set_copy_toolbar_active_arrows(self): if self.copy_button.get_active(): self.forward_image.set_visible(True) @@ -3377,36 +3410,36 @@ class RapidApp(dbus.service.Object): def on_copy_button_toggle_event(self, radio_button): self._set_copy_toolbar_active_arrows() self.prefs.auto_delete = not self.copy_button.get_active() - + def _set_from_toolbar_state(self): logger.debug("_set_from_toolbar_state") self.auto_detect_button.set_active(self.prefs.device_autodetection) if self.prefs.device_autodetection: self.from_filechooser_button.set_sensitive(False) self.from_filechooser_button.set_current_folder(self.prefs.device_location) - + def on_auto_detect_button_toggled_event(self, button): logger.debug("on_auto_detect_button_toggled_event") self.from_filechooser_button.set_sensitive(not button.get_active()) if not self.rerun_setup_available_image_and_video_media: self.prefs.device_autodetection = button.get_active() - + def on_from_filechooser_button_selection_changed(self, filechooserbutton): logger.debug("on_from_filechooser_button_selection_changed") path = filechooserbutton.get_current_folder() if path and not self.rerun_setup_available_image_and_video_media: self.prefs.device_location = path - + def on_to_photo_filechooser_button_selection_changed(self, filechooserbutton): path = filechooserbutton.get_current_folder() if path: self.prefs.download_folder = path - + def on_to_video_filechooser_button_selection_changed(self, filechooserbutton): path = filechooserbutton.get_current_folder() if path: self.prefs.video_download_folder = path - + def _set_to_toolbar_values(self): self.to_photo_filechooser_button.set_current_folder(self.prefs.download_folder) self.to_video_filechooser_button.set_current_folder(self.prefs.video_download_folder) @@ -3419,20 +3452,20 @@ class RapidApp(dbus.service.Object): def _set_window_size(self): """ Remember the window size from the last time the program was run, or - set a default size + set a default size """ - + if self.prefs.main_window_maximized: self.rapidapp.maximize() - self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH, + self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH, config.DEFAULT_WINDOW_HEIGHT) elif self.prefs.main_window_size_x > 0: self.rapidapp.set_default_size(self.prefs.main_window_size_x, self.prefs.main_window_size_y) else: # set a default size - self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH, + self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH, config.DEFAULT_WINDOW_HEIGHT) - + def _set_device_collection_size(self): """ @@ -3445,36 +3478,36 @@ class RapidApp(dbus.service.Object): else: # don't allow the media collection to be absolutely empty self.device_collection_scrolledwindow.set_size_request(-1, 47) - - + + def on_rapidapp_window_state_event(self, widget, event): """ Records the window maximization state in the preferences.""" - + if event.changed_mask & gdk.WINDOW_STATE_MAXIMIZED: self.prefs.main_window_maximized = event.new_window_state & gdk.WINDOW_STATE_MAXIMIZED - + def _setup_buttons(self): thumbnails_button = self.builder.get_object("thumbnails_button") image = gtk.image_new_from_file(paths.share_dir('glade3/thumbnails_icon.png')) thumbnails_button.set_image(image) - + preview_button = self.builder.get_object("preview_button") image = gtk.image_new_from_file(paths.share_dir('glade3/photo_icon.png')) preview_button.set_image(image) - + next_image_button = self.builder.get_object("next_image_button") image = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_BUTTON) next_image_button.set_image(image) - + prev_image_button = self.builder.get_object("prev_image_button") image = gtk.image_new_from_stock(gtk.STOCK_GO_BACK, gtk.ICON_SIZE_BUTTON) prev_image_button.set_image(image) - + def _setup_icons(self): icons = ['rapid-photo-downloader-jobcode',] - icon_list = [(icon, paths.share_dir('glade3/%s.svg' % icon)) for icon in icons] + icon_list = [(icon, paths.share_dir('glade3/%s.svg' % icon)) for icon in icons] register_iconsets(icon_list) - + def _setup_error_icons(self): """ hide display of warning and error symbols in the taskbar until they @@ -3486,7 +3519,7 @@ class RapidApp(dbus.service.Object): self.error_image.hide() self.warning_image.hide() self.warning_vseparator.hide() - + def enable_prefs_and_refresh(self, enabled): """ If enable is true, then the user is able to activate the preferences @@ -3495,20 +3528,20 @@ class RapidApp(dbus.service.Object): """ self.refresh_action.set_sensitive(enabled) self.preferences_action.set_sensitive(enabled) - + def statusbar_message(self, msg): self.rapid_statusbar.push(self.statusbar_context_id, msg) - + def statusbar_message_remove(self): self.rapid_statusbar.pop(self.statusbar_context_id) def display_backup_mounts(self): """ - Create a message to be displayed to the user showing which backup + Create a message to be displayed to the user showing which backup mounts will be used """ message = '' - + paths = self.backup_devices.keys() i = 0 v = len(paths) @@ -3521,22 +3554,22 @@ class RapidApp(dbus.service.Object): prefix = " " + _("and") + " " i += 1 message = "%s%s'%s'" % (message, prefix, self.backup_devices[b][0].get_name()) - + if v > 1: message = _("Using backup devices") + " %s" % message elif v == 1: message = _("Using backup device") + " %s" % message else: message = _("No backup devices detected") - + return message - + def display_free_space(self): """ - Displays the amount of space free on the filesystem the files will be + Displays the amount of space free on the filesystem the files will be downloaded to. - - Also displays backup volumes / path being used. + + Also displays backup volumes / path being used. """ photo_dir = self.is_valid_download_dir(path=self.prefs.download_folder, is_photo_dir=True, show_error_in_log=True) video_dir = self.is_valid_download_dir(path=self.prefs.video_download_folder, is_photo_dir=False, show_error_in_log=True) @@ -3545,17 +3578,17 @@ class RapidApp(dbus.service.Object): self.prefs.video_download_folder) else: same_file_system = False - + dirs = [] if photo_dir: dirs.append((self.prefs.download_folder, _("photos"))) if video_dir and not same_file_system: dirs.append((self.prefs.video_download_folder, _("videos"))) - + msg = '' if len(dirs) > 1: msg = ' ' + _('Free space:') + ' ' - + for i in range(len(dirs)): dir_info = dirs[i] folder = gio.File(dir_info[0]) @@ -3563,13 +3596,13 @@ class RapidApp(dbus.service.Object): size = file_info.get_attribute_uint64(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE) free = format_size_for_user(bytes=size) if len(dirs) > 1: - #(videos) or (photos) will be appended to the free space message displayed to the + #(videos) or (photos) will be appended to the free space message displayed to the #user in the status bar. - #you should only translate this if your language does not use parantheses + #you should only translate this if your language does not use parantheses file_type = _("(%(file_type)s)") % {'file_type': dir_info[1]} #Freespace available on the filesystem for downloading to - #Displayed in status bar message on main window + #Displayed in status bar message on main window msg += _("%(free)s %(file_type)s") % {'free': free, 'file_type': file_type} if i == 0: #Inserted in the middle of the statusbar message concerning the amount of freespace @@ -3579,17 +3612,17 @@ class RapidApp(dbus.service.Object): elif not self.prefs.backup_images: #Inserted at the end of the statusbar message concerning the amount of freespace #Used to differentiate between two different file systems - #e.g. Free space: 21.3GB (photos); 14.7GB (videos). + #e.g. Free space: 21.3GB (photos); 14.7GB (videos). msg += _(".") - + else: #Freespace available on the filesystem for downloading to #Displayed in status bar message on main window #e.g. 14.7GB available msg = " " + _("%(free)s free") % {'free': free} - - - if self.prefs.backup_images: + + + if self.prefs.backup_images: if not self.prefs.backup_device_autodetection: if self.prefs.backup_location == self.prefs.backup_video_location: if DOWNLOAD_VIDEO: @@ -3604,29 +3637,29 @@ class RapidApp(dbus.service.Object): 'path':self.prefs.backup_location, 'path2': self.prefs.backup_video_location} else: - msg2 = self.display_backup_mounts() - + msg2 = self.display_backup_mounts() + if msg: msg = _("%(freespace)s. %(backuppaths)s.") % {'freespace': msg, 'backuppaths': msg2} else: msg = msg2 - + msg = msg.rstrip() - + self.statusbar_message(msg) - + def log_error(self, severity, problem, details, extra_detail=None): """ Display error and warning messages to user in log window """ self.error_log.add_message(severity, problem, details, extra_detail) - - + + def on_error_eventbox_button_press_event(self, widget, event): self.prefs.show_log_dialog = True - self.error_log.widget.show() - - + self.error_log.widget.show() + + def on_menu_log_window_toggled(self, widget): active = widget.get_active() self.prefs.show_log_dialog = active @@ -3634,14 +3667,14 @@ class RapidApp(dbus.service.Object): self.error_log.widget.show() else: self.error_log.widget.hide() - + def notify_prefs_are_invalid(self, details): title = _("Program preferences are invalid") logger.critical(title) self.log_error(severity=config.CRITICAL_ERROR, problem=title, details=details) - - + + # # # # Utility functions # # # @@ -3650,7 +3683,7 @@ class RapidApp(dbus.service.Object): """ 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 """ @@ -3665,7 +3698,7 @@ class RapidApp(dbus.service.Object): return False else: return i - + def size_files_to_be_downloaded(self, files): """ Returns the total sizes of the photos and videos to be downloaded in bytes @@ -3679,12 +3712,12 @@ class RapidApp(dbus.service.Object): video_size += rpd_file.size return (photo_size, video_size) - + def check_download_folder_validity(self, files_by_scan_pid): """ Checks validity of download folders based on the file types the user is attempting to download. - + If valid, returns a tuple of True and an empty list. If invalid, returns a tuple of False and a list of the invalid directores. """ @@ -3702,27 +3735,27 @@ class RapidApp(dbus.service.Object): if not need_video_folder: if self.files_of_type_present(files, rpdfile.FILE_TYPE_VIDEO): need_video_folder = True - + # second, check validity if need_photo_folder: - if not self.is_valid_download_dir(self.prefs.download_folder, + if not self.is_valid_download_dir(self.prefs.download_folder, is_photo_dir=True): valid = False invalid_dirs.append(self.prefs.download_folder) else: - logger.debug("Photo download folder is valid: %s", + logger.debug("Photo download folder is valid: %s", self.prefs.download_folder) - + if need_video_folder: if not self.is_valid_download_dir(self.prefs.video_download_folder, - is_photo_dir=False): + is_photo_dir=False): valid = False invalid_dirs.append(self.prefs.video_download_folder) else: - logger.debug("Video download folder is valid: %s", + logger.debug("Video download folder is valid: %s", self.prefs.video_download_folder) - + return (valid, invalid_dirs) def same_file_system(self, file1, file2): @@ -3735,27 +3768,27 @@ class RapidApp(dbus.service.Object): f2_info = f2.query_info(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) f2_id = f2_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) return f1_id == f2_id - - + + def same_file(self, file1, file2): """Returns True if the files / directories are the same """ f1 = gio.File(file1) f2 = gio.File(file2) - + file_attributes = "id::file" f1_info = f1.query_filesystem_info(file_attributes) f1_id = f1_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILE) f2_info = f2.query_filesystem_info(file_attributes) f2_id = f2_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILE) return f1_id == f2_id - + def is_valid_download_dir(self, path, is_photo_dir, show_error_in_log=False): """ Checks the following conditions: Does the directory exist? Is it writable? - + if show_error_in_log is True, then display warning in log window, using is_photo_dir, which if true means the download directory is for photos, if false, for Videos @@ -3765,11 +3798,11 @@ class RapidApp(dbus.service.Object): download_folder_type = _("Photo") else: download_folder_type = _("Video") - + try: d = gio.File(path) if not d.query_exists(cancellable=None): - logger.error("%s download folder does not exist: %s", + logger.error("%s download folder does not exist: %s", download_folder_type, path) if show_error_in_log: severity = config.WARNING @@ -3781,16 +3814,16 @@ class RapidApp(dbus.service.Object): file_attributes = "standard::type,access::can-read,access::can-write" file_info = d.query_filesystem_info(file_attributes) file_type = file_info.get_file_type() - + if file_type != gio.FILE_TYPE_DIRECTORY and file_type != gio.FILE_TYPE_UNKNOWN: - logger.error("%s download folder is invalid: %s", + logger.error("%s download folder is invalid: %s", download_folder_type, path) if show_error_in_log: severity = config.WARNING problem = _("%(file_type)s download folder is invalid") % { 'file_type': download_folder_type} details = _("Folder: %s") % path - self.log_error(severity, problem, details) + self.log_error(severity, problem, details) else: # is the directory writable? try: @@ -3803,7 +3836,7 @@ class RapidApp(dbus.service.Object): problem = _("%(file_type)s download folder is not writable") % { 'file_type': download_folder_type} details = _("Folder: %s") % path - self.log_error(severity, problem, details) + self.log_error(severity, problem, details) else: f = gio.File(temp_dir) f.delete(cancellable=None) @@ -3811,66 +3844,66 @@ class RapidApp(dbus.service.Object): except gio.Error, inst: logger.error("Error checking download directory %s", path) logger.error(inst) - + return valid - - - + + + # # # # Process results and management # # # - - + + def _start_process_managers(self): """ Set up process managers. - + A task such as scanning a device or copying files is handled in its own process. """ - + self.batch_size = 10 self.batch_size_MB = 2 - + sequence_values = (self.downloads_today_value, self.downloads_today_date_value, self.day_start_value, self.refresh_downloads_today_value, - self.stored_sequence_value, + self.stored_sequence_value, self.uses_stored_sequence_no_value, self.uses_session_sequece_no_value, self.uses_sequence_letter_value) - - # daemon process to rename files and create subfolders + + # daemon process to rename files and create subfolders self.subfolder_file_manager = SubfolderFileManager( self.subfolder_file_results, sequence_values) - + # process to scan source devices / paths - self.scan_manager = ScanManager(self.scan_results, self.batch_size, + self.scan_manager = ScanManager(self.scan_results, self.batch_size, self.device_collection.add_device) - + #process to copy files from source to destination - self.copy_files_manager = CopyFilesManager(self.copy_files_results, + self.copy_files_manager = CopyFilesManager(self.copy_files_results, self.batch_size_MB) - + #process to back files up self.backup_manager = BackupFilesManager(self.backup_results, self.batch_size_MB) - + #process to enhance files after they've been copied and before they're #renamed self.file_modify_manager = FileModifyManager(self.file_modify_results) - - + + def scan_results(self, source, condition): """ Receive results from scan processes """ connection = self.scan_manager.get_pipe(source) - + conn_type, data = connection.recv() - + if conn_type == rpdmp.CONN_COMPLETE: connection.close() self.scan_manager.no_tasks -= 1 @@ -3883,7 +3916,7 @@ class RapidApp(dbus.service.Object): self.device_collection.update_device(scan_pid, size) self.device_collection.update_progress(scan_pid, 0.0, results_summary, 0, pulse=False) self.set_download_action_sensitivity() - + if (not self.auto_start_is_on and self.prefs.generate_thumbnails): self.download_progressbar.set_text(_("Thumbnails")) @@ -3897,7 +3930,7 @@ class RapidApp(dbus.service.Object): logger.debug("Turning on display of thumbnails") self.display_scan_thumbnails() self.download_button.grab_focus() - + # signal that no more data is coming, finishing io watch for this pipe return False else: @@ -3910,20 +3943,20 @@ class RapidApp(dbus.service.Object): scanning_progress = file_type_counter.running_file_count() self.device_collection.update_device(scan_pid, size) self.device_collection.update_progress(scan_pid, 0.0, scanning_progress, 0, pulse=True) - + for rpd_file in rpd_files: - self.thumbnails.add_file(rpd_file=rpd_file, + self.thumbnails.add_file(rpd_file=rpd_file, generate_thumbnail = not self.auto_start_is_on) - + # must return True for this method to be called again return True - + @dbus.service.method (config.DBUS_NAME, in_signature='', out_signature='b') def is_running (self): return self.running - + @dbus.service.method (config.DBUS_NAME, in_signature='', out_signature='') def start (self): @@ -3932,14 +3965,14 @@ class RapidApp(dbus.service.Object): else: self.running = True gtk.main() - + def start(): is_beta = config.version.find('~') > 0 - + parser = OptionParser(version= "%%prog %s" % utilities.human_readable_version(config.version)) parser.set_defaults(verbose=is_beta, extensions=False) - # Translators: this text is displayed to the user when they request information on the command line options. + # Translators: this text is displayed to the user when they request information on the command line options. # The text %default should not be modified or left out. parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help=_("display program information on the command line as the program runs (default: %default)")) parser.add_option("-d", "--debug", action="store_true", dest="debug", help=_('display debugging information when run from the command line')) @@ -3951,26 +3984,26 @@ def start(): parser.add_option("-l", "--device-location", type="string", metavar="PATH", dest="device_location", help=_("manually specify the PATH of the device from which to download, overwriting existing program preferences")) parser.add_option("--reset-settings", action="store_true", dest="reset", help=_("reset all program settings and preferences and exit")) (options, args) = parser.parse_args() - + if options.debug: logging_level = logging.DEBUG elif options.verbose: logging_level = logging.INFO else: logging_level = logging.ERROR - + logger.setLevel(logging_level) - + if options.auto_detect and options.device_location: logger.info(_("Error: specify device auto-detection or manually specify a device's path from which to download, but do not do both.")) sys.exit(1) - + if options.auto_detect: auto_detect=True logger.info("Device auto detection set from command line") else: auto_detect=None - + if options.device_location: device_location=options.device_location if device_location[-1]=='/': @@ -3987,15 +4020,15 @@ def start(): v += '%s, ' % e.upper() v = file_type + " " + v[:-1] + ' '+ (_('and %s') % exts[-1].upper()) print v - + sys.exit(0) - + if options.reset: 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: @@ -4017,7 +4050,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: + if request != dbus.bus.REQUEST_NAME_REPLY_EXISTS: app = RapidApp(bus, '/', config.DBUS_NAME, focal_length=focal_length, auto_detect=auto_detect, device_location=device_location) else: @@ -4025,8 +4058,8 @@ def start(): print "Rapid Photo Downloader is already running" object = bus.get_object (config.DBUS_NAME, "/") app = dbus.Interface (object, config.DBUS_NAME) - - app.start() + + app.start() if __name__ == "__main__": start() diff --git a/rapid/rpdfile.py b/rapid/rpdfile.py index cf38ebc..a405782 100644 --- a/rapid/rpdfile.py +++ b/rapid/rpdfile.py @@ -43,7 +43,8 @@ import thumbnail as tn RAW_EXTENSIONS = ['arw', 'dcr', 'cr2', 'crw', 'dng', 'mos', 'mef', 'mrw', - 'nef', 'orf', 'pef', 'raf', 'raw', 'rw2', 'sr2', 'srw'] + 'nef', 'nrw', 'orf', 'pef', 'raf', 'raw', 'rw2', 'sr2', + 'srw'] JPEG_EXTENSIONS = ['jpg', 'jpe', 'jpeg'] diff --git a/rapid/subfolderfile.py b/rapid/subfolderfile.py index a7b0f89..4e23309 100644 --- a/rapid/subfolderfile.py +++ b/rapid/subfolderfile.py @@ -26,6 +26,8 @@ Runs as a daemon process. import os, datetime, collections +import shutil +import errno import gio import multiprocessing import logging @@ -44,7 +46,7 @@ from gettext import gettext as _ class SyncRawJpeg: def __init__(self): self.photos = {} - + def add_download(self, name, extension, date_time, sub_seconds, sequence_number_used): if name not in self.photos: self.photos[name] = ([extension], date_time, sub_seconds, sequence_number_used) @@ -52,16 +54,16 @@ class SyncRawJpeg: if extension not in self.photos[name][0]: self.photos[name][0].append(extension) - + def matching_pair(self, name, extension, date_time, sub_seconds): """Checks to see if the image matches an image that has already been downloaded. Image name (minus extension), exif date time, and exif subseconds are checked. - + Returns -1 and a sequence number if the name, extension, and exif values match (i.e. it has already been downloaded) Returns 0 and a sequence number if name and exif values match, but the extension is different (i.e. a matching RAW + JPG image) Returns -99 and a sequence number of None if photos detected with the same filenames, but taken at different times Returns 1 and a sequence number of None if no match""" - + if name in self.photos: if self.photos[name][1] == date_time and self.photos[name][2] == sub_seconds: if extension in self.photos[name][0]: @@ -71,37 +73,37 @@ class SyncRawJpeg: else: return (-99, None) return (1, None) - + def ext_exif_date_time(self, name): """Returns first extension, exif date time and subseconds data for the already downloaded photo""" return (self.photos[name][0][0], self.photos[name][1], self.photos[name][2]) - + def time_subseconds_human_readable(date, subseconds): return _("%(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \ {'hour':date.strftime("%H"), - 'minute':date.strftime("%M"), + 'minute':date.strftime("%M"), 'second':date.strftime("%S"), - 'subsecond': subseconds} + 'subsecond': subseconds} def load_metadata(rpd_file, temp_file=True): """ Loads the metadata for the file. Returns True if operation succeeded, false otherwise - + If temp_file is true, the the metadata from the temporary file rather than the original source file is used. This is important, because the metadata can be modified by the filemodify process. """ - if rpd_file.metadata is None: + if rpd_file.metadata is None: if not rpd_file.load_metadata(temp_file): # Error in reading metadata rpd_file.add_problem(None, pn.CANNOT_DOWNLOAD_BAD_METADATA, {'filetype': rpd_file.title_capitalized}) return False return True - + def _generate_name(generator, rpd_file): - + do_generation = True if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: do_generation = load_metadata(rpd_file) @@ -115,37 +117,37 @@ def _generate_name(generator, rpd_file): value = '' else: value = '' - + return value def generate_subfolder(rpd_file): - + if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: generator = gn.PhotoSubfolder(rpd_file.subfolder_pref_list) else: generator = gn.VideoSubfolder(rpd_file.subfolder_pref_list) - + rpd_file.download_subfolder = _generate_name(generator, rpd_file) return rpd_file - + def generate_name(rpd_file): do_generation = True - + if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: generator = gn.PhotoName(rpd_file.name_pref_list) else: generator = gn.VideoName(rpd_file.name_pref_list) - + rpd_file.download_name = _generate_name(generator, rpd_file) return rpd_file - + class SubfolderFile(multiprocessing.Process): def __init__(self, results_pipe, sequence_values): multiprocessing.Process.__init__(self) self.daemon = True self.results_pipe = results_pipe - + self.downloads_today = sequence_values[0] self.downloads_today_date = sequence_values[1] self.day_start = sequence_values[2] @@ -154,12 +156,17 @@ class SubfolderFile(multiprocessing.Process): self.uses_stored_sequence_no = sequence_values[5] self.uses_session_sequece_no = sequence_values[6] self.uses_sequence_letter = sequence_values[7] - + # As of Ubuntu 12.10 / Fedora 18, the file move/rename command is running agonisingly slowly + # A hackish workaround is to replace it with the standard python function + self.use_gnome_file_operations = False + logger.debug("Start of day is set to %s", self.day_start.value) - + def progress_callback_no_update(self, amount_downloaded, total): pass - + #~ if debug_progress: + #~ logger.debug("%.1f", amount_downloaded / float(total)) + def file_exists(self, rpd_file, identifier=None): """ Notify user that the download file already exists @@ -171,24 +178,24 @@ class SubfolderFile(multiprocessing.Process): date = dt.strftime("%x") time = dt.strftime("%X") except: - logger.warning("Could not determine the file modification time of %s", + logger.warning("Could not determine the file modification time of %s", rpd_file.download_full_file_name) date = time = '' - + if not identifier: - rpd_file.add_problem(None, pn.FILE_ALREADY_EXISTS_NO_DOWNLOAD, + rpd_file.add_problem(None, pn.FILE_ALREADY_EXISTS_NO_DOWNLOAD, {'filetype':rpd_file.title_capitalized}) - rpd_file.add_extra_detail(pn.EXISTING_FILE, - {'filetype': rpd_file.title, + rpd_file.add_extra_detail(pn.EXISTING_FILE, + {'filetype': rpd_file.title, 'date': date, 'time': time}) rpd_file.status = config.STATUS_DOWNLOAD_FAILED rpd_file.error_extra_detail = pn.extra_detail_definitions[pn.EXISTING_FILE] % \ {'date':date, 'time':time, 'filetype': rpd_file.title} else: - rpd_file.add_problem(None, pn.UNIQUE_IDENTIFIER_ADDED, + rpd_file.add_problem(None, pn.UNIQUE_IDENTIFIER_ADDED, {'filetype':rpd_file.title_capitalized}) - rpd_file.add_extra_detail(pn.UNIQUE_IDENTIFIER, - {'identifier': identifier, + rpd_file.add_extra_detail(pn.UNIQUE_IDENTIFIER, + {'identifier': identifier, 'filetype': rpd_file.title, 'date': date, 'time': time}) rpd_file.status = config.STATUS_DOWNLOADED_WITH_WARNING @@ -197,10 +204,10 @@ class SubfolderFile(multiprocessing.Process): 'date': date, 'time': time} rpd_file.error_title = rpd_file.problem.get_title() rpd_file.error_msg = _("Source: %(source)s\nDestination: %(destination)s") \ - % {'source': rpd_file.full_file_name, + % {'source': rpd_file.full_file_name, 'destination': rpd_file.download_full_file_name} return rpd_file - + def download_failure_file_error(self, rpd_file, inst): """ Handle cases where file failed to download @@ -209,24 +216,49 @@ class SubfolderFile(multiprocessing.Process): rpd_file.add_extra_detail(pn.DOWNLOAD_COPYING_ERROR_DETAIL, inst) rpd_file.status = config.STATUS_DOWNLOAD_FAILED logger.error("Failed to create file %s: %s", rpd_file.download_full_file_name, inst) - + rpd_file.error_title = rpd_file.problem.get_title() rpd_file.error_msg = _("%(problem)s\nFile: %(file)s") % \ {'problem': rpd_file.problem.get_problems(), 'file': rpd_file.full_file_name} - + return rpd_file - + + def download_file_exists(self, rpd_file): + """ + Check how to handle a download file already existing + """ + if (rpd_file.download_conflict_resolution == + config.ADD_UNIQUE_IDENTIFIER): + add_unique_identifier = True + logger.debug("Will add unique identifier to avoid duplicate filename") + else: + rpd_file = self.file_exists(rpd_file) + add_unique_identifier = False + return (rpd_file, add_unique_identifier) + + def added_unique_identifier(self, rpd_file): + """ + Track fact that a unique identifier was added to a file name + """ + move_succeeded = True + suffix_already_used = False + rpd_file = self.file_exists(rpd_file, identifier) + logger.error("%s: %s - %s", rpd_file.full_file_name, + rpd_file.problem.get_title(), + rpd_file.problem.get_problems()) + return (rpd_file, move_succeeded, suffix_already_used) + def same_name_different_exif(self, sync_photo_name, rpd_file): """Notify the user that a file was already downloaded with the same name, but the exif information was different""" i1_ext, i1_date_time, i1_subseconds = self.sync_raw_jpeg.ext_exif_date_time(sync_photo_name) - detail = {'image1': "%s%s" % (sync_photo_name, i1_ext), + detail = {'image1': "%s%s" % (sync_photo_name, i1_ext), 'image1_date': i1_date_time.strftime("%x"), - 'image1_time': time_subseconds_human_readable(i1_date_time, i1_subseconds), - 'image2': rpd_file.name, + 'image1_time': time_subseconds_human_readable(i1_date_time, i1_subseconds), + 'image2': rpd_file.name, 'image2_date': rpd_file.metadata.date_time().strftime("%x"), 'image2_time': time_subseconds_human_readable( - rpd_file.metadata.date_time(), + rpd_file.metadata.date_time(), rpd_file.metadata.sub_seconds())} rpd_file.add_problem(None, pn.SAME_FILE_DIFFERENT_EXIF, detail) @@ -234,8 +266,8 @@ class SubfolderFile(multiprocessing.Process): rpd_file.error_msg = pn.problem_definitions[pn.SAME_FILE_DIFFERENT_EXIF][1] % detail rpd_file.status = config.STATUS_DOWNLOADED_WITH_WARNING return rpd_file - - + + def run(self): """ Get subfolder and name. @@ -246,37 +278,39 @@ class SubfolderFile(multiprocessing.Process): """ i = 0 download_count = 0 - + duplicate_files = {} - # Track downloads today, using a class whose purpose is to + # Track downloads today, using a class whose purpose is to # take the value in the user prefs, increment, and then be used # to update the prefs (which can only happen via the main process) self.downloads_today_tracker = prefsrapid.DownloadsTodayTracker( day_start = self.day_start.value, downloads_today = self.downloads_today.value, downloads_today_date = self.downloads_today_date.value) - + # Track sequences using shared downloads today and stored sequence number # (shared with main process) - self.sequences = gn.Sequences(self.downloads_today_tracker, + self.sequences = gn.Sequences(self.downloads_today_tracker, self.stored_sequence_no.value) - + self.sync_raw_jpeg = SyncRawJpeg() - + while True: - logger.debug("Finished %s. Getting next task.", download_count) + if download_count: + logger.debug("Finished %s. Getting next task.", download_count) - # rename file and move to generated subfolder + # rename file and move to generated subfolder download_succeeded, download_count, rpd_file = self.results_pipe.recv() - + move_succeeded = False - + if download_succeeded: - temp_file = gio.File(rpd_file.temp_full_file_name) + if self.use_gnome_file_operations: + temp_file = gio.File(rpd_file.temp_full_file_name) synchronize_raw_jpg_failed = False if not (rpd_file.synchronize_raw_jpg and @@ -290,8 +324,8 @@ class SubfolderFile(multiprocessing.Process): synchronize_raw_jpg_failed = True else: j, sequence_to_use = self.sync_raw_jpeg.matching_pair( - name=sync_photo_name, extension=sync_photo_ext, - date_time=rpd_file.metadata.date_time(), + name=sync_photo_name, extension=sync_photo_ext, + date_time=rpd_file.metadata.date_time(), sub_seconds=rpd_file.metadata.sub_seconds()) if j == -1: # this exact file has already been downloaded (same extension, same filename, and same exif date time subsecond info) @@ -306,26 +340,29 @@ class SubfolderFile(multiprocessing.Process): self.sequences.set_matched_sequence_value(sequence_to_use) if j == -99: rpd_file = self.same_name_different_exif(sync_photo_name, rpd_file) - + if synchronize_raw_jpg_failed: generation_succeeded = False else: # Generate subfolder name and new file name generation_succeeded = True - + if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: if hasattr(rpd_file, 'new_focal_length'): # A RAW file has had its focal length and aperture adjusted. # These have been written out to an XMP sidecar, but they won't - # be picked up by pyexiv2. So temporarily change the values inplace here, + # be picked up by pyexiv2. So temporarily change the values inplace here, # without saving them. if load_metadata(rpd_file): rpd_file.metadata["Exif.Photo.FocalLength"] = rpd_file.new_focal_length rpd_file.metadata["Exif.Photo.FNumber"] = rpd_file.new_aperture - + rpd_file = generate_subfolder(rpd_file) + + if rpd_file.download_subfolder: - + logger.debug("Generated subfolder name %s for file %s", rpd_file.download_subfolder, rpd_file.name) + if self.refresh_downloads_today.value: # overwrite downloads today value tracked here, # as user has modified their preferences @@ -333,21 +370,26 @@ class SubfolderFile(multiprocessing.Process): self.downloads_today_tracker.set_raw_downloads_today_date(self.downloads_today_date.value) self.downloads_today_tracker.day_start = self.day_start.value self.refresh_downloads_today.value = False - + # update whatever the stored value is self.sequences.stored_sequence_no = self.stored_sequence_no.value rpd_file.sequences = self.sequences - + # generate the file name rpd_file = generate_name(rpd_file) - + if rpd_file.has_problem(): + logger.debug("Encountered a problem generating file name for file %s", rpd_file.name) rpd_file.status = config.STATUS_DOWNLOADED_WITH_WARNING rpd_file.error_title = rpd_file.problem.get_title() rpd_file.error_msg = _("%(problem)s\nFile: %(file)s") % \ {'problem': rpd_file.problem.get_problems(), 'file': rpd_file.full_file_name} - + else: + logger.debug("Generated file name %s for file %s", rpd_file.download_name, rpd_file.name) + else: + logger.debug("Failed to generate subfolder name for file: %s", rpd_file.name) + # Check for any errors if not rpd_file.download_subfolder or not rpd_file.download_name: if not rpd_file.download_subfolder and not rpd_file.download_name: @@ -360,59 +402,93 @@ class SubfolderFile(multiprocessing.Process): rpd_file.add_extra_detail(pn.NO_DATA_TO_NAME, {'filetype': area}) generation_succeeded = False rpd_file.status = config.STATUS_DOWNLOAD_FAILED - + rpd_file.error_title = rpd_file.problem.get_title() rpd_file.error_msg = _("%(problem)s\nFile: %(file)s") % \ {'problem': rpd_file.problem.get_problems(), 'file': rpd_file.full_file_name} - - + + if generation_succeeded: rpd_file.download_path = os.path.join(rpd_file.download_folder, rpd_file.download_subfolder) rpd_file.download_full_file_name = os.path.join(rpd_file.download_path, rpd_file.download_name) rpd_file.download_full_base_name = os.path.splitext(rpd_file.download_full_file_name)[0] - + subfolder = gio.File(path=rpd_file.download_path) - + # Create subfolder if it does not exist. # It is possible to skip the query step, and just try to create # the directories and ignore the error of it already existing - # but it takes twice as long to fail with an error than just # run the straight query - + + logger.debug("Probing to see if subfolder already exists...") if not subfolder.query_exists(cancellable=None): - try: - subfolder.make_directory_with_parents(cancellable=gio.Cancellable()) - except gio.Error, inst: - # The directory may have been created by another process - # between the time it takes to query and the time it takes - # to create a new directory. Ignore such errors. - if inst.code <> gio.ERROR_EXISTS: - logger.error("Failed to create download subfolder: %s", rpd_file.download_path) - logger.error(inst) - rpd_file.error_title = _("Failed to create download subfolder") - rpd_file.error_msg = _("Path: %s") % rpd_file.download_path - + if self.use_gnome_file_operations: + try: + logger.debug("...subfolder doesn't exist: creating it using gnome...") + subfolder.make_directory_with_parents(cancellable=gio.Cancellable()) + logger.debug("...subfolder created") + except gio.Error, inst: + # The directory may have been created by another process + # between the time it takes to query and the time it takes + # to create a new directory. Ignore such errors. + if inst.code <> gio.ERROR_EXISTS: + logger.error("Failed to create download subfolder: %s", rpd_file.download_path) + logger.error(inst) + rpd_file.error_title = _("Failed to create download subfolder") + rpd_file.error_msg = _("Path: %s") % rpd_file.download_path + else: + try: + logger.debug("...subfolder doesn't exist: creating it using python...") + os.makedirs(rpd_file.download_path) + logger.debug("...subfolder created") + except IOError as inst: + if inst.errno <> errno.EEXIST: + logger.error("Failed to create download subfolder: %s", rpd_file.download_path) + logger.error(inst) + rpd_file.error_title = _("Failed to create download subfolder") + rpd_file.error_msg = _("Path: %s") % rpd_file.download_path + else: + logger.debug("...subfolder already exists") + # Move temp file to subfolder - download_file = gio.File(rpd_file.download_full_file_name) - add_unique_identifier = False - try: - temp_file.move(download_file, self.progress_callback_no_update, cancellable=None) - move_succeeded = True - if rpd_file.status <> config.STATUS_DOWNLOADED_WITH_WARNING: - rpd_file.status = config.STATUS_DOWNLOADED - except gio.Error, inst: - if inst.code == gio.ERROR_EXISTS: - if (rpd_file.download_conflict_resolution == - config.ADD_UNIQUE_IDENTIFIER): - add_unique_identifier = True + if self.use_gnome_file_operations: + download_file = gio.File(rpd_file.download_full_file_name) + try: + logger.debug("Attempting to use Gnome to rename file %s to %s .....", rpd_file.temp_full_file_name, rpd_file.download_full_file_name) + temp_file.move(download_file, self.progress_callback_no_update, cancellable=None) + logger.debug("....successfully renamed file") + move_succeeded = True + if rpd_file.status <> config.STATUS_DOWNLOADED_WITH_WARNING: + rpd_file.status = config.STATUS_DOWNLOADED + except gio.Error as inst: + if inst.code == gio.ERROR_EXISTS: + rpd_file, add_unique_identifier = download_file_exists(rpd_file) else: - rpd_file = self.file_exists(rpd_file) - else: - rpd_file = self.download_failure_file_error(rpd_file, inst) - + rpd_file = self.download_failure_file_error(rpd_file, inst) + else: + # Use python library functions to rename file + # Sadly this code basically duplicates the logic of the previous block + try: + if os.path.exists(rpd_file.download_full_file_name): + raise IOError(errno.EEXIST, "File exists: %s" % rpd_file.download_full_file_name) + logger.debug("Attempting to use python to rename file %s to %s .....", rpd_file.temp_full_file_name, rpd_file.download_full_file_name) + shutil.move(rpd_file.temp_full_file_name, rpd_file.download_full_file_name) + logger.debug("....successfully renamed file") + move_succeeded = True + if rpd_file.status <> config.STATUS_DOWNLOADED_WITH_WARNING: + rpd_file.status = config.STATUS_DOWNLOADED + except IOError as inst: + if inst.errno == errno.EEXIST: + rpd_file, add_unique_identifier = self.download_file_exists(rpd_file) + else: + rpd_file = self.download_failure_file_error(rpd_file, inst.strerror) + except: + rpd_file = self.download_failure_file_error(rpd_file, inst.strerror) + if add_unique_identifier: name = os.path.splitext(rpd_file.download_name) full_name = rpd_file.download_full_file_name @@ -425,25 +501,35 @@ class SubfolderFile(multiprocessing.Process): rpd_file.download_full_file_name = os.path.join( rpd_file.download_path, rpd_file.download_name) - download_file = gio.File( + + if self.use_gnome_file_operations: + download_file = gio.File( rpd_file.download_full_file_name) - - try: - temp_file.move(download_file, self.progress_callback_no_update, cancellable=None) - move_succeeded = True - suffix_already_used = False - rpd_file = self.file_exists(rpd_file, identifier) - logger.error("%s: %s - %s", rpd_file.full_file_name, - rpd_file.problem.get_title(), - rpd_file.problem.get_problems()) - except gio.Error, inst: - if inst.code <> gio.ERROR_EXISTS: + try: + temp_file.move(download_file, self.progress_callback_no_update, cancellable=None) + rpd_file, move_succeeded, suffix_already_used = self.added_unique_identifier(rpd_file) + except gio.Error, inst: + if inst.code <> gio.ERROR_EXISTS: + rpd_file = self.download_failure_file_error(rpd_file, inst) + else: + try: + if os.path.exists(rpd_file.download_full_file_name): + raise IOError(errno.EEXIST, "File exists: %s" % rpd_file.download_full_file_name) + shutil.move(rpd_file.temp_full_file_name, rpd_file.download_full_file_name) + rpd_file, move_succeeded, suffix_already_used = self.added_unique_identifier(rpd_file) + except IOError as inst: + if inst.errno <> errno.EEXIST: + rpd_file = self.download_failure_file_error(rpd_file, inst) + break + except: rpd_file = self.download_failure_file_error(rpd_file, inst) - - - - logger.debug("Finish processing file: %s", download_count) - + break + + + + + logger.debug("Finish processing file: %s", download_count) + if move_succeeded: if synchronize_raw_jpg: if sequence_to_use is None: @@ -458,17 +544,15 @@ class SubfolderFile(multiprocessing.Process): if sequence_to_use is None: if self.uses_session_sequece_no.value or self.uses_sequence_letter.value: self.sequences.increment( - self.uses_session_sequece_no.value, + self.uses_session_sequece_no.value, self.uses_sequence_letter.value) if self.uses_stored_sequence_no.value: self.stored_sequence_no.value += 1 self.downloads_today_tracker.increment_downloads_today() self.downloads_today.value = self.downloads_today_tracker.get_raw_downloads_today() self.downloads_today_date.value = self.downloads_today_tracker.get_raw_downloads_today_date() - + if rpd_file.temp_thm_full_name: - # copy and rename THM video file - source = gio.File(path=rpd_file.temp_thm_full_name) ext = None if hasattr(rpd_file, 'thm_extension'): if rpd_file.thm_extension: @@ -476,45 +560,69 @@ class SubfolderFile(multiprocessing.Process): if ext is None: ext = '.THM' download_thm_full_name = rpd_file.download_full_base_name + ext - dest = gio.File(path=download_thm_full_name) - try: - source.move(dest, self.progress_callback_no_update, cancellable=None) - rpd_file.download_thm_full_name = download_thm_full_name - except gio.Error, inst: - logger.error("Failed to move video THM file %s", download_thm_full_name) - + + # copy and rename THM video file + if self.use_gnome_file_operations: + source = gio.File(path=rpd_file.temp_thm_full_name) + dest = gio.File(path=download_thm_full_name) + try: + source.move(dest, self.progress_callback_no_update, cancellable=None) + rpd_file.download_thm_full_name = download_thm_full_name + except gio.Error, inst: + logger.error("Failed to move video THM file %s", download_thm_full_name) + else: + try: + # don't check to see if it already exists + shutil.move(rpd_file.temp_thm_full_name, download_thm_full_name) + rpd_file.download_thm_full_name = download_thm_full_name + except: + logger.error("Failed to move video THM file %s", download_thm_full_name) + if rpd_file.temp_xmp_full_name: # copy and rename XMP sidecar file - source = gio.File(path=rpd_file.temp_xmp_full_name) # generate_name() has generated xmp extension with correct capitalization download_xmp_full_name = rpd_file.download_full_base_name + rpd_file.xmp_extension - dest = gio.File(path=download_xmp_full_name) - try: - source.move(dest, self.progress_callback_no_update, cancellable=None) - rpd_file.download_xmp_full_name = download_xmp_full_name - except gio.Error, inst: - logger.error("Failed to move XMP sidecar file %s", download_xmp_full_name) - - + if self.use_gnome_file_operations: + source = gio.File(path=rpd_file.temp_xmp_full_name) + dest = gio.File(path=download_xmp_full_name) + try: + source.move(dest, self.progress_callback_no_update, cancellable=None) + rpd_file.download_xmp_full_name = download_xmp_full_name + except gio.Error, inst: + logger.error("Failed to move XMP sidecar file %s", download_xmp_full_name) + else: + try: + shutil.move(rpd_file.temp_xmp_full_name, download_xmp_full_name) + rpd_file.download_xmp_full_name = download_xmp_full_name + except: + logger.error("Failed to move XMP sidecar file %s", download_xmp_full_name) + + if not move_succeeded: - logger.error("%s: %s - %s", rpd_file.full_file_name, - rpd_file.problem.get_title(), + logger.error("%s: %s - %s", rpd_file.full_file_name, + rpd_file.problem.get_title(), rpd_file.problem.get_problems()) - try: - temp_file.delete(cancellable=None) - except gio.Error, inst: - logger.error("Failed to delete temporary file %s", rpd_file.temp_full_file_name) - logger.error(inst) - - - - - + if self.use_gnome_file_operations: + try: + temp_file.delete(cancellable=None) + except gio.Error, inst: + logger.error("Failed to delete temporary file %s", rpd_file.temp_full_file_name) + logger.error(inst) + else: + try: + os.remove(rpd_file.temp_full_file_name) + except: + logger.error("Failed to delete temporary file %s", rpd_file.temp_full_file_name) + + + + + rpd_file.metadata = None #purge metadata, as it cannot be pickled rpd_file.sequences = None - self.results_pipe.send((move_succeeded, rpd_file,)) - + self.results_pipe.send((move_succeeded, rpd_file, download_count)) + i += 1 - + |