diff options
author | Julien Valroff <julien@kirya.net> | 2011-04-08 07:09:54 +0200 |
---|---|---|
committer | Julien Valroff <julien@kirya.net> | 2011-04-08 07:09:54 +0200 |
commit | eb4c5cc4472b16ce10401611140381e5ba5b6aca (patch) | |
tree | 6e52c0a981186a09ab8161a6bc99a999f32ac408 /rapid | |
parent | 6866d4a5b74779f087b8e44148a49163d8b7327b (diff) |
Imported Upstream version 0.4.0~alpha4upstream/0.4.0_alpha4
Diffstat (limited to 'rapid')
-rw-r--r-- | rapid/ChangeLog | 52 | ||||
-rw-r--r-- | rapid/INSTALL | 31 | ||||
-rw-r--r-- | rapid/config.py | 2 | ||||
-rw-r--r-- | rapid/copyfiles.py | 33 | ||||
-rw-r--r-- | rapid/downloadtracker.py | 78 | ||||
-rw-r--r-- | rapid/errorlog.py | 92 | ||||
-rw-r--r-- | rapid/generatename.py | 55 | ||||
-rw-r--r-- | rapid/glade3/errorlog.ui | 90 | ||||
-rw-r--r-- | rapid/glade3/rapid.ui | 3 | ||||
-rw-r--r-- | rapid/metadatavideo.py | 17 | ||||
-rw-r--r-- | rapid/preferencesdialog.py | 232 | ||||
-rw-r--r-- | rapid/prefsrapid.py | 9 | ||||
-rwxr-xr-x | rapid/rapid.py | 198 | ||||
-rw-r--r-- | rapid/rpdfile.py | 39 | ||||
-rwxr-xr-x | rapid/scan.py | 2 | ||||
-rw-r--r-- | rapid/subfolderfile.py | 323 |
16 files changed, 1007 insertions, 249 deletions
diff --git a/rapid/ChangeLog b/rapid/ChangeLog index 0234613..86037ed 100644 --- a/rapid/ChangeLog +++ b/rapid/ChangeLog @@ -1,3 +1,51 @@ +Version 0.4.0 alpha 4 +--------------------- + +2011-04-04 + +Fixed bug #750808: errorlog.ui not included in setup.py. + + +Version 0.4.0 alpha 3 +--------------------- + +2011-04-04 + +Features added since alpha 2: + +* Error log window to display download warnings and errors. +* Synchronize RAW + JPEG Sequence values. + +Fixed bug #739021: unable to set subfolder and file rename preferences on +alpha and beta Linux distributions such as Ubuntu 11.04 or Fedora 15. + +Updated Brazilian, Dutch, French, German and Spanish translations. + +Version 0.4.0 alpha 2 +--------------------- + +2011-03-31 + +Features added since alpha 1: + +* Sample file names and subfolders are now displayed in the preferences dialog + window. +* The option to add a unique identifier to a filename if a file with the same + name already exists + +Other changes: + +* Updated INSTALL file to match new package requirements. + +* Added program icon to main window. + +* Bug fix: leave file preview mode when download devices are changed in the + preferences. + +* Bug fix: don't crash on startup when trying to display free space and photo or + video download folders do not exist. + + Version 0.4.0 alpha 1 --------------------- @@ -35,7 +83,9 @@ want to use Frames Per Second or Codec metadata information in subfolder or video file names, you must ensure it is installed. This is no longer checked at program startup. -Thanks go to Robert Park for refreshing the tranlsations code. +Thanks go to Robert Park for refreshing the translations code. + +Added Romanian translation. Version 0.3.5 diff --git a/rapid/INSTALL b/rapid/INSTALL index 8270a20..8cde587 100644 --- a/rapid/INSTALL +++ b/rapid/INSTALL @@ -1,24 +1,15 @@ -Rapid Photo Downloader depends on the following software: - -- GNOME 2.18 or higher -- GTK+ 2.10 or higher -- Python 2.5 or 2.6 -- pygtk 2.12 or higher -- python-gconf 2.18 or higher -- python-glade2 2.12 or higher -- gnome-python 2.12 or higher -- libexiv2 0.15 or higher -- pyexiv2 0.1.1 or higher +Rapid Photo Downloader requires the following software: -To run Rapid Photo Downloader you will need all the software mentioned above. - -If you want to see dropshadows around thumbnail images, install python-imaging. -This is optional but recommended. +* Python 2.6 or 2.7 +* Pyexiv2 0.3.0 or higher +* python-gnome2 2.28 or higher +* python-gtk2 2.17 or higher +* python-gconf 2.28 or higher +* python-notify 0.1.1 or higher +* python-imaging 1.1.7 or higher +* librsvg2-common 2.26 or higher -A strongly recommended package is exiv2 (not just the library libexiv2), so -Rapid Photo Downloader can determine if it can download additional types of RAW -files (some early versions of exiv2 and pyexiv2 segfault on certain RAW file -types). +To run Rapid Photo Downloader you will need all the software mentioned above. If you want to download videos, you should install: @@ -29,7 +20,7 @@ If you want to download videos, you should install: hachoir metadata is required to download videos. kaa metadata is used to extract additional metadata from videos. ffmpegthumbnailer is used only to display thumbnail images before the download occurs. This is a useful feature, -and if you can install it, it is recommended. +and if you can install it, it is strongly recommended. hachoir metadata, kaa metadata and ffmpegthumbnailer are optional. The program will run without them. diff --git a/rapid/config.py b/rapid/config.py index 9f0b6b1..d020562 100644 --- a/rapid/config.py +++ b/rapid/config.py @@ -15,7 +15,7 @@ ### along with this program; if not, write to the Free Software ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -version = '0.4.0~a1' +version = '0.4.0~a4' GCONF_KEY="/apps/rapid-photo-downloader" diff --git a/rapid/copyfiles.py b/rapid/copyfiles.py index f9ebc60..78fe8a3 100644 --- a/rapid/copyfiles.py +++ b/rapid/copyfiles.py @@ -64,16 +64,21 @@ class CopyFiles(multiprocessing.Process): def update_progress(self, amount_downloaded, total): - if (amount_downloaded - self.bytes_downloaded > self.batch_size_bytes) or (amount_downloaded == total): - chunk_downloaded = amount_downloaded - self.bytes_downloaded - self.bytes_downloaded = amount_downloaded - self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_BYTES, (self.scan_pid, self.total_downloaded + amount_downloaded)))) + # first check if process is being terminated + if not self.terminate_queue.empty(): + # it is - cancel the current copy + self.cancel_copy.cancel() + else: + if (amount_downloaded - self.bytes_downloaded > self.batch_size_bytes) or (amount_downloaded == total): + chunk_downloaded = amount_downloaded - self.bytes_downloaded + self.bytes_downloaded = amount_downloaded + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_BYTES, (self.scan_pid, self.total_downloaded + amount_downloaded)))) def progress_callback(self, amount_downloaded, total): - if self.check_termination_request(): - # FIXME: cancel copy - pass + #~ if self.check_termination_request(): + #~ # FIXME: cancel copy + #~ pass self.update_progress(amount_downloaded, total) @@ -84,6 +89,8 @@ class CopyFiles(multiprocessing.Process): self.bytes_downloaded = 0 self.total_downloaded = 0 + self.cancel_copy = gio.Cancellable() + self.create_temp_dirs() # Send the location of both temporary directories, so they can be @@ -113,7 +120,7 @@ class CopyFiles(multiprocessing.Process): copy_succeeded = False try: - source.copy(dest, self.progress_callback, cancellable=None) + source.copy(dest, self.progress_callback, cancellable=self.cancel_copy) copy_succeeded = True except gio.Error, inst: rpd_file.add_problem(None, @@ -124,6 +131,12 @@ class CopyFiles(multiprocessing.Process): {'errorno': inst.code, 'strerror': inst.message}) rpd_file.status = config.STATUS_DOWNLOAD_FAILED + + rpd_file.error_title = rpd_file.problem.get_title() + rpd_file.error_msg = _("%(problem)s\nFile: %(file)s") % \ + {'problem': rpd_file.problem.get_problems(), + 'file': rpd_file.full_file_name} + logger.error("Failed to download file: %s", rpd_file.full_file_name) logger.error(inst) self.update_progress(rpd_file.size, rpd_file.size) @@ -132,7 +145,9 @@ class CopyFiles(multiprocessing.Process): # succeeded or not. It's neccessary to keep the user informed. self.total_downloaded += rpd_file.size - + if rpd_file.metadata is not None: + rpd_file.metadata = None + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_FILE, (copy_succeeded, rpd_file, i + 1, temp_full_file_name)))) diff --git a/rapid/downloadtracker.py b/rapid/downloadtracker.py new file mode 100644 index 0000000..f2c80e2 --- /dev/null +++ b/rapid/downloadtracker.py @@ -0,0 +1,78 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch <damonlynch@gmail.com> + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +class DownloadTracker: + def __init__(self): + self.size_of_download_in_bytes_by_scan_pid = dict() + self.total_bytes_copied_in_bytes_by_scan_pid = dict() + self.no_files_in_download_by_scan_pid = dict() + self.file_types_present_by_scan_pid = dict() + self.download_count_for_file_by_unique_id = dict() + self.download_count_by_scan_pid = dict() + self.rename_chunk = dict() + self.files_downloaded = dict() + + def init_stats(self, scan_pid, bytes, no_files): + self.no_files_in_download_by_scan_pid[scan_pid] = no_files + self.rename_chunk[scan_pid] = bytes / 10 / no_files + self.size_of_download_in_bytes_by_scan_pid[scan_pid] = bytes + self.rename_chunk[scan_pid] * no_files + self.files_downloaded[scan_pid] = 0 + + def get_no_files_in_download(self, scan_pid): + return self.no_files_in_download_by_scan_pid[scan_pid] + + def file_downloaded_increment(self, scan_pid): + self.files_downloaded[scan_pid] += 1 + + + def get_percent_complete(self, scan_pid): + """ + Returns a float representing how much of the download + has been completed + """ + + # three components: copy (download), rename, and backup + percent_complete = ((float( + self.total_bytes_copied_in_bytes_by_scan_pid[scan_pid]) + + self.rename_chunk[scan_pid] * self.files_downloaded[scan_pid]) + / self.size_of_download_in_bytes_by_scan_pid[scan_pid]) * 100 + return percent_complete + + def set_total_bytes_copied(self, scan_pid, total_bytes): + self.total_bytes_copied_in_bytes_by_scan_pid[scan_pid] = total_bytes + + def set_download_count_for_file(self, unique_id, download_count): + self.download_count_for_file_by_unique_id[unique_id] = download_count + + def get_download_count_for_file(self, unique_id): + return self.download_count_for_file_by_unique_id[unique_id] + + def set_download_count(self, scan_pid, download_count): + self.download_count_by_scan_pid[scan_pid] = download_count + + def get_file_types_present(self, scan_pid): + return self.file_types_present_by_scan_pid[scan_pid] + + def set_file_types_present(self, scan_pid, file_types_present): + self.file_types_present_by_scan_pid[scan_pid] = file_types_present + + def purge(self, scan_pid): + del self.no_files_in_download_by_scan_pid[scan_pid] + del self.size_of_download_in_bytes_by_scan_pid[scan_pid] + diff --git a/rapid/errorlog.py b/rapid/errorlog.py new file mode 100644 index 0000000..0334465 --- /dev/null +++ b/rapid/errorlog.py @@ -0,0 +1,92 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch <damonlynch@gmail.com> + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import gtk + +import pango +import config +import paths + +class ErrorLog(): + """ + Displays a log of errors, warnings or other information to the user + """ + + def __init__(self, rapidapp): + """ + Initialize values for log dialog, but do not display. + """ + + self.builder = gtk.Builder() + self.builder.add_from_file(paths.share_dir("glade3/errorlog.ui")) + self.builder.connect_signals(self) + self.widget = self.builder.get_object("errorlog") + self.log_textview = self.builder.get_object("log_textview") + self.log_scrolledwindow = self.builder.get_object("log_scrolledwindow") + + self.widget.connect("delete-event", self.hide_window) + + self.rapidapp = rapidapp + #~ self.log_textview.set_cursor_visible(False) + self.textbuffer = self.log_textview.get_buffer() + + self.error_tag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD, foreground="red") + self.warning_tag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD) + self.extra_detail_tag = self.textbuffer.create_tag(style=pango.STYLE_ITALIC) + + def add_message(self, severity, problem, details, extra_detail): + if severity in [config.CRITICAL_ERROR, config.SERIOUS_ERROR]: + self.rapidapp.error_image.show() + elif severity == config.WARNING: + self.rapidapp.warning_image.show() + self.rapidapp.warning_vseparator.show() + + iter = self.textbuffer.get_end_iter() + if severity in [config.CRITICAL_ERROR, config.SERIOUS_ERROR]: + self.textbuffer.insert_with_tags(iter, problem +"\n", self.error_tag) + else: + self.textbuffer.insert_with_tags(iter, problem +"\n", self.warning_tag) + if details: + iter = self.textbuffer.get_end_iter() + self.textbuffer.insert(iter, details + "\n") + if extra_detail: + iter = self.textbuffer.get_end_iter() + self.textbuffer.insert_with_tags(iter, extra_detail +"\n", self.extra_detail_tag) + + iter = self.textbuffer.get_end_iter() + self.textbuffer.insert(iter, "\n") + + # move viewport to display the latest message + adjustment = self.log_scrolledwindow.get_vadjustment() + adjustment.set_value(adjustment.upper) + + + def on_errorlog_response(self, dialog, arg): + if arg == gtk.RESPONSE_CLOSE: + pass + self.rapidapp.error_image.hide() + self.rapidapp.warning_image.hide() + self.rapidapp.warning_vseparator.hide() + self.rapidapp.prefs.show_log_dialog = False + self.widget.hide() + return True + + def hide_window(self, window, event): + window.hide() + return True diff --git a/rapid/generatename.py b/rapid/generatename.py index be4ea6e..e904a70 100644 --- a/rapid/generatename.py +++ b/rapid/generatename.py @@ -17,7 +17,7 @@ ### along with this program; if not, write to the Free Software ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -import os, re, datetime, string +import os, re, datetime, string, collections import multiprocessing import logging @@ -267,8 +267,7 @@ class PhotoName: def _get_component(self): - #~ try: - if True: + try: if self.L0 == DATE_TIME: return self._get_date_component() elif self.L0 == TEXT: @@ -283,12 +282,9 @@ class PhotoName: return self.rpd_file.job_code elif self.L0 == SEPARATOR: return os.sep - else: - # for development phase only - return '' - #~ except: - #~ self.rpd_file.add_problem(self.component, pn.ERROR_IN_GENERATION, _(self.L0)) - #~ return '' + except: + self.rpd_file.add_problem(self.component, pn.ERROR_IN_GENERATION, _(self.L0)) + return '' def generate_name(self, rpd_file): @@ -418,12 +414,28 @@ class Sequences: self.session_sequence_no = 0 self.sequence_letter = -1 self.downloads_today_tracker = downloads_today_tracker - self.stored_sequence_no = stored_sequence_no + 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): @@ -433,6 +445,12 @@ class Sequences: 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 @@ -440,6 +458,23 @@ class Sequences: 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', + 'session_sequence_no sequence_letter downloads_today stored_sequence_no' + ) + sequences.session_sequence_no = self._get_session_sequence_no() + sequences.sequence_letter = self._get_sequence_letter() + sequences.downloads_today = self._get_downloads_today() + sequences.stored_sequence_no = self._get_stored_sequence_no() + return sequences diff --git a/rapid/glade3/errorlog.ui b/rapid/glade3/errorlog.ui new file mode 100644 index 0000000..43c8bdd --- /dev/null +++ b/rapid/glade3/errorlog.ui @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="2.20"/> + <!-- interface-naming-policy project-wide --> + <object class="GtkDialog" id="errorlog"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Error Log</property> + <property name="default_width">650</property> + <property name="default_height">400</property> + <property name="destroy_with_parent">True</property> + <property name="icon">rapid-photo-downloader.svg</property> + <property name="type_hint">dialog</property> + <signal name="response" handler="on_errorlog_response" swapped="no"/> + <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> + <object class="GtkButton" id="button1"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_action_appearance">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </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="GtkScrolledWindow" id="log_scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkViewport" id="viewport1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkTextView" id="log_textview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="editable">False</property> + <property name="cursor_visible">False</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button1</action-widget> + </action-widgets> + </object> +</interface> diff --git a/rapid/glade3/rapid.ui b/rapid/glade3/rapid.ui index 50306f0..1b587b4 100644 --- a/rapid/glade3/rapid.ui +++ b/rapid/glade3/rapid.ui @@ -90,6 +90,7 @@ <object class="GtkWindow" id="rapidapp"> <property name="can_focus">False</property> <property name="title" translatable="yes">Rapid Photo Downloader</property> + <property name="icon">rapid-photo-downloader.svg</property> <signal name="destroy" handler="on_rapidapp_destroy" swapped="no"/> <signal name="window-state-event" handler="on_rapidapp_window_state_event" swapped="no"/> <child> @@ -280,6 +281,7 @@ <property name="use_action_appearance">False</property> <property name="label" translatable="yes">_Error Log</property> <property name="use_underline">True</property> + <signal name="toggled" handler="on_menu_log_window_toggled" swapped="no"/> </object> </child> <child> @@ -885,6 +887,7 @@ <object class="GtkEventBox" id="error_eventbox"> <property name="visible">True</property> <property name="can_focus">False</property> + <signal name="button-press-event" handler="on_error_eventbox_button_press_event" swapped="no"/> <child> <object class="GtkHBox" id="hbox6"> <property name="visible">True</property> diff --git a/rapid/metadatavideo.py b/rapid/metadatavideo.py index 144686a..7b6bc6c 100644 --- a/rapid/metadatavideo.py +++ b/rapid/metadatavideo.py @@ -41,7 +41,6 @@ try: except ImportError: DOWNLOAD_VIDEO = False - if DOWNLOAD_VIDEO: def version_info(): @@ -76,12 +75,9 @@ if DOWNLOAD_VIDEO: self.filename = filename self.u_filename = unicodeFilename(filename) - self.parser = createParser(self.u_filename, self.filename) - self.metadata = extractMetadata(self.parser) - + self.metadata = None def _kaa_get(self, key, missing, stream=None): - if not hasattr(self, 'info'): try: from kaa.metadata import parse @@ -104,7 +100,14 @@ It is needed to access FPS and codec video file metadata.""" else: return missing + def _load_hachoir_metadata_parser(self): + self.parser = createParser(self.u_filename, self.filename) + self.metadata = extractMetadata(self.parser) + def _get(self, key, missing): + if self.metadata is None: + self._load_hachoir_metadata_parser() + try: v = self.metadata.get(key) except: @@ -174,11 +177,11 @@ class DummyMetaData(): See VideoMetaData class for documentation of class methods. """ - def __init__(self): + def __init__(self, filename): pass def date_time(self, missing=''): - return date_time.date_time.now() + return datetime.datetime.now() def codec(self, stream=0, missing=''): return 'H.264 AVC' diff --git a/rapid/preferencesdialog.py b/rapid/preferencesdialog.py index 5ad257d..4909820 100644 --- a/rapid/preferencesdialog.py +++ b/rapid/preferencesdialog.py @@ -22,6 +22,7 @@ import datetime import gtk +import datetime import multiprocessing import logging logger = multiprocessing.get_logger() @@ -31,6 +32,7 @@ import misc import config import paths +import rpdfile import higdefaults as hd import metadataphoto import metadatavideo @@ -42,7 +44,7 @@ import generatename as gn from generatenameconfig import * import problemnotification as pn -from prefsrapid import format_pref_list_for_pretty_print +from prefsrapid import format_pref_list_for_pretty_print, DownloadsTodayTracker from gettext import gettext as _ @@ -186,6 +188,8 @@ class Comboi18n(gtk.ComboBox): cell = gtk.CellRendererText() self.pack_start(cell, True) self.add_attribute(cell, 'text', 1) + # must name the combo box on pygtk used in Ubuntu 11.04, Fedora 15, etc. + self.set_name('GtkComboBox') def append_text(self, text): model = self.get_model() @@ -449,8 +453,7 @@ class PhotoRenameTable(tpm.TablePlusMinus): self.get_preferencesdialog_prefs() - self.get_prefs_factory() - #~ self.prefs_factory.setDownloadStartTime(datetime.datetime.now()) + self.setup_prefs_factory() try: self.prefs_factory.check_prefs_for_validity() @@ -466,7 +469,7 @@ class PhotoRenameTable(tpm.TablePlusMinus): # reset them to their default self.pref_list = self.prefs_factory.default_prefs - self.get_prefs_factory() + self.setup_prefs_factory() self.update_parentapp_prefs() msg = "%s.\n" % e @@ -535,7 +538,7 @@ class PhotoRenameTable(tpm.TablePlusMinus): self.pref_list = self.preferencesdialog.prefs.image_rename - def get_prefs_factory(self): + def setup_prefs_factory(self): self.prefs_factory = PhotoNamePrefs(self.pref_list) def update_parentapp_prefs(self): @@ -548,7 +551,7 @@ class PhotoRenameTable(tpm.TablePlusMinus): #~ self.prefs_factory.setJobCode(job_code) def update_example(self): - self.preferencesdialog.update_image_rename_example() + self.preferencesdialog.update_photo_rename_example() def get_default_row(self): return self.prefs_factory.get_default_row() @@ -618,7 +621,7 @@ class VideoRenameTable(PhotoRenameTable): def get_preferencesdialog_prefs(self): self.pref_list = self.preferencesdialog.prefs.video_rename - def get_prefs_factory(self): + def setup_prefs_factory(self): self.prefs_factory = VideoNamePrefs(self.pref_list) def update_parentapp_prefs(self): @@ -639,7 +642,7 @@ class SubfolderTable(PhotoRenameTable): def get_preferencesdialog_prefs(self): self.pref_list = self.preferencesdialog.prefs.subfolder - def get_prefs_factory(self): + def setup_prefs_factory(self): self.prefs_factory = PhotoSubfolderPrefs(self.pref_list) def update_parentapp_prefs(self): @@ -656,7 +659,7 @@ class VideoSubfolderTable(PhotoRenameTable): def get_preferencesdialog_prefs(self): self.pref_list = self.preferencesdialog.prefs.video_subfolder - def get_prefs_factory(self): + def setup_prefs_factory(self): self.prefs_factory = VideoSubfolderPrefs(self.pref_list) def update_parentapp_prefs(self): @@ -787,7 +790,7 @@ class JobCodeDialog(gtk.Dialog): self.post_job_code_entry_callback(self, user_chose_code, self.get_job_code()) -class PreferencesDialog(gtk.Window): +class PreferencesDialog(): """ Dialog window to show Rapid Photo Downloader preferences. @@ -808,6 +811,8 @@ class PreferencesDialog(gtk.Window): rapidapp.preferences_dialog_displayed = True + self.pref_dialog_startup = True + self.rapidapp = rapidapp self._setup_tab_selector() @@ -819,31 +824,7 @@ class PreferencesDialog(gtk.Window): else: self.file_types = _("photos") - # get example photo and video data - #~ try: - #~ w = workers.firstWorkerReadyToDownload() - #~ mediaFile = w.firstImage() - #~ self.sample_image_name = mediaFile.name - #~ # assume the metadata is already read - #~ self.sample_image = mediaFile.metadata - #~ except: - self.sample_image = metadataphoto.DummyMetaData() - self.sample_image_name = 'IMG_0524.CR2' - - if not metadatavideo.DOWNLOAD_VIDEO: - self.sample_video = None - self.sample_video_name = '' - else: - #~ try: - #~ mediaFile = w.firstVideo() - #~ self.sample_video_name = mediaFile.name - #~ self.sample_video = mediaFile.metadata - #~ self.videoFallBackDate = mediaFile.modificationTime - #~ except: - self.sample_video = metadatavideo.DummyMetaData() - self.sample_video_name = 'MVI_1379.MOV' - #~ self.videoFallBackDate = datetime.datetime.now() - + self._setup_sample_names() # setup tabs self._setup_photo_download_folder_tab() @@ -875,6 +856,8 @@ class PreferencesDialog(gtk.Window): self.rename_scrolledwindow.set_size_request(self.rename_table.allocation.width + extra, -1) self.dialog.show() + + self.pref_dialog_startup = False def __getattr__(self, key): """Allow builder widgets to be accessed as self.widgetname @@ -887,7 +870,7 @@ class PreferencesDialog(gtk.Window): def on_preferencesdialog_destroy(self, widget): """ Delete variables from memory that cause a file descriptor to be created on a mounted media""" - del self.sample_image, self.rename_table.prefs_factory, self.subfolder_table.prefs_factory + logger.debug("Preference window closing") def _setup_tab_selector(self): self.notebook.set_show_tabs(0) @@ -922,7 +905,49 @@ class PreferencesDialog(gtk.Window): def on_device_location_filechooser_button_selection_changed(self, widget): self.prefs.device_location = widget.get_current_folder() + + def _setup_sample_names(self, use_dummy_data = False): + """ + If use_dummy_data is True, then samples will not attempt to get + data from actual download files + """ + self.downloads_today_tracker = DownloadsTodayTracker( + day_start = self.prefs.day_start, + downloads_today = self.prefs.downloads_today[1], + downloads_today_date = self.prefs.downloads_today[0]) + self.sequences = gn.Sequences(self.downloads_today_tracker, + self.prefs.stored_sequence_no) + + # get example photo and video data + if use_dummy_data: + self.sample_photo = None + else: + self.sample_photo = self.rapidapp.thumbnails.get_sample_file(rpdfile.FILE_TYPE_PHOTO) + if self.sample_photo is not None: + # try to load metadata from the file returned + # if it fails, give up with this sample file + if not self.sample_photo.load_metadata(): + self.sample_photo = None + else: + self.sample_photo.sequences = self.sequences + self.sample_photo.download_start_time = datetime.datetime.now() + + if self.sample_photo is None: + self.sample_photo = rpdfile.SamplePhoto(sequences=self.sequences) + self.sample_video = None + if metadatavideo.DOWNLOAD_VIDEO: + if not use_dummy_data: + self.sample_video = self.rapidapp.thumbnails.get_sample_file(rpdfile.FILE_TYPE_VIDEO) + if self.sample_video is not None: + self.sample_video.load_metadata() + self.sample_video.sequences = self.sequences + self.sample_video.download_start_time = datetime.datetime.now() + if self.sample_video is None: + self.sample_video = rpdfile.SampleVideo(sequences=self.sequences) + + + def _setup_control_spacing(self): """ set spacing of some but not all controls @@ -1002,16 +1027,25 @@ class PreferencesDialog(gtk.Window): self.rename_table = PhotoRenameTable(self, self.rename_scrolledwindow) self.rename_table_vbox.pack_start(self.rename_table) self.rename_table.show_all() - self.original_name_label.set_markup("<i>%s</i>" % self.sample_image_name) - self.update_image_rename_example() - + self._setup_photo_original_name() + self.update_photo_rename_example() + + def _setup_photo_original_name(self): + self.original_name_label.set_markup("<i>%s</i>" % self.sample_photo.display_name) + def _setup_video_rename_tab(self): self.video_rename_table = VideoRenameTable(self, self.video_rename_scrolledwindow) self.video_rename_table_vbox.pack_start(self.video_rename_table) self.video_rename_table.show_all() - self.video_original_name_label.set_markup("<i>%s</i>" % self.sample_video_name) + self._setup_video_original_name() self.update_video_rename_example() + + def _setup_video_original_name(self): + if self.sample_video is not None: + self.video_original_name_label.set_markup("<i>%s</i>" % self.sample_video.display_name) + else: + self.video_original_name_label.set_markup("") def _setup_rename_options_tab(self): @@ -1154,77 +1188,100 @@ class PreferencesDialog(gtk.Window): self.backup_duplicate_skip_radiobutton.set_active(True) - def update_example_file_name(self, display_table, rename_table, sample, sampleName, example_label, fallback_date = None): - problem = pn.Problem() - name = 'file.cr2' - #~ if hasattr(self, display_table): - #~ rename_table.update_example_job_code() - #~ rename_table.prefs_factory.initializeProblem(problem) - #~ name = rename_table.prefs_factory.generateNameUsingPreferences( - #~ sample, sampleName, - #~ self.prefs.strip_characters, sequencesPreliminary=False, fallback_date=fallback_date) - #~ else: - #~ name = '' + def update_example_file_name(self, display_table, rename_table, sample_rpd_file, generator, example_label): + if hasattr(self, display_table) and sample_rpd_file is not None: + sample_rpd_file.download_folder = self.prefs.get_download_folder_for_file_type(sample_rpd_file.file_type) + sample_rpd_file.strip_characters = self.prefs.strip_characters + sample_rpd_file.initialize_problem() + name = generator.generate_name(sample_rpd_file) + else: + name = '' # since this is markup, escape it text = "<i>%s</i>" % utilities.escape(name) - if problem.has_problem(): - text += "\n" - # Translators: please do not modify or leave out html formatting tags like <i> and <b>. These are used to format the text the users sees - text += _("<i><b>Warning:</b> There is insufficient metadata to fully generate the name. Please use other renaming options.</i>") + if sample_rpd_file is not None: + if sample_rpd_file.has_problem(): + text += "\n" + # Translators: please do not modify or leave out html formatting tags like <i> and <b>. These are used to format the text the users sees + text += _("<i><b>Warning:</b> There is insufficient metadata to fully generate the name. Please use other renaming options.</i>") example_label.set_markup(text) - def update_image_rename_example(self): + def update_photo_rename_example(self): """ Displays example image name to the user """ - self.update_example_file_name('rename_table', self.rename_table, self.sample_image, self.sample_image_name, self.new_name_label) + generator = gn.PhotoName(self.prefs.image_rename) + self.update_example_file_name('rename_table', self.rename_table, + self.sample_photo, generator, + self.new_name_label) def update_video_rename_example(self): """ Displays example video name to the user """ - self.update_example_file_name('video_rename_table', self.video_rename_table, self.sample_video, self.sample_video_name, self.video_new_name_label) + if self.sample_video is not None: + generator = gn.VideoName(self.prefs.video_rename) + else: + generator = None + self.update_example_file_name('video_rename_table', + self.video_rename_table, + self.sample_video, generator, + self.video_new_name_label) - def update_download_folder_example(self, display_table, subfolder_table, download_folder, sample, sampleName, example_download_path_label, subfolder_warning_label): + def update_download_folder_example(self, display_table, subfolder_table, + download_folder, sample_rpd_file, + generator, + example_download_path_label, + subfolder_warning_label): """ Displays example subfolder name(s) to the user """ - problem = pn.Problem() - #~ if hasattr(self, display_table): - #~ subfolder_table.update_example_job_code() + if hasattr(self, display_table) and sample_rpd_file is not None: #~ subfolder_table.update_example_job_code() - #~ subfolder_table.prefs_factory.initializeProblem(problem) - #~ path = subfolder_table.prefs_factory.generateNameUsingPreferences( - #~ sample, sampleName, - #~ self.prefs.strip_characters, fallback_date = fallback_date) - #~ else: - #~ path = '' + sample_rpd_file.strip_characters = self.prefs.strip_characters + sample_rpd_file.initialize_problem() + path = generator.generate_name(sample_rpd_file) + else: + path = '' - path = 'sample' - text = os.path.join(download_folder, path) # since this is markup, escape it path = utilities.escape(text) - if problem.has_problem(): - warning = _("<i><b>Warning:</b> There is insufficient metadata to fully generate subfolders. Please use other subfolder naming options.</i>" ) - else: - warning = "" + + warning = "" + if sample_rpd_file is not None: + if sample_rpd_file.has_problem(): + warning = _("<i><b>Warning:</b> There is insufficient metadata to fully generate subfolders. Please use other subfolder naming options.</i>" ) + # Translators: you should not modify or leave out the %s. This is a code used by the programming language python to insert a value that thes user will see example_download_path_label.set_markup(_("<i>Example: %s</i>") % text) subfolder_warning_label.set_markup(warning) def update_photo_download_folder_example(self): if hasattr(self, 'subfolder_table'): - self.update_download_folder_example('subfolder_table', self.subfolder_table, self.prefs.download_folder, self.sample_image, self.sample_image_name, self.example_photo_download_path_label, self.photo_subfolder_warning_label) + generator = gn.PhotoSubfolder(self.prefs.subfolder) + self.update_download_folder_example('subfolder_table', + self.subfolder_table, self.prefs.download_folder, + self.sample_photo, generator, + self.example_photo_download_path_label, + self.photo_subfolder_warning_label) def update_video_download_folder_example(self): if hasattr(self, 'video_subfolder_table'): - self.update_download_folder_example('video_subfolder_table', self.video_subfolder_table, self.prefs.video_download_folder, self.sample_video, self.sample_video_name, self.example_video_download_path_label, self.video_subfolder_warning_label) + if self.sample_video is not None: + generator = gn.VideoSubfolder(self.prefs.video_subfolder) + else: + generator = None + self.update_download_folder_example('video_subfolder_table', + self.video_subfolder_table, + self.prefs.video_download_folder, + self.sample_video, generator, + self.example_video_download_path_label, + self.video_subfolder_warning_label) def on_hour_spinbutton_value_changed(self, spinbutton): hour = spinbutton.get_value_as_int() @@ -1252,7 +1309,7 @@ class PreferencesDialog(gtk.Window): v = 0 self.rapidapp.downloads_today_tracker.reset_downloads_today(v) self.rapidapp.refresh_downloads_today = True - self.update_image_rename_example() + self.update_photo_rename_example() def on_stored_number_entry_changed(self, entry): # do not update value if a download is occurring - it will mess it up! @@ -1269,7 +1326,7 @@ class PreferencesDialog(gtk.Window): if v < 0: v = 0 self.prefs.stored_sequence_no = v - self.update_image_rename_example() + self.update_photo_rename_example() def _update_subfolder_pref_on_error(self, new_pref_list): self.prefs.subfolder = new_pref_list @@ -1364,7 +1421,7 @@ class PreferencesDialog(gtk.Window): selection.select_path((row,)) self.update_job_codes() - self.update_image_rename_example() + self.update_photo_rename_example() self.update_video_rename_example() self.update_photo_download_folder_example() self.update_video_download_folder_example() @@ -1377,7 +1434,7 @@ class PreferencesDialog(gtk.Window): if user_selected: self.job_code_liststore.clear() self.update_job_codes() - self.update_image_rename_example() + self.update_photo_rename_example() self.update_video_rename_example() self.update_photo_download_folder_example() self.update_video_download_folder_example() @@ -1386,7 +1443,7 @@ class PreferencesDialog(gtk.Window): iter = self.job_code_liststore.get_iter(path) self.job_code_liststore.set_value(iter, 0, new_text) self.update_job_codes() - self.update_image_rename_example() + self.update_photo_rename_example() self.update_video_rename_example() self.update_photo_download_folder_example() self.update_video_download_folder_example() @@ -1447,7 +1504,7 @@ class PreferencesDialog(gtk.Window): def on_strip_characters_checkbutton_toggled(self, check_button): self.prefs.strip_characters = check_button.get_active() - self.update_image_rename_example() + self.update_photo_rename_example() self.update_photo_download_folder_example() self.update_video_download_folder_example() @@ -1462,6 +1519,7 @@ class PreferencesDialog(gtk.Window): """ Sets sensitivity of image device controls """ + controls = [self.device_location_explanation_label, self.device_location_label, self.device_location_filechooser_button] @@ -1476,6 +1534,16 @@ class PreferencesDialog(gtk.Window): c.set_sensitive(True) self.autodetect_psd_checkbutton.set_sensitive(False) self.autodetect_image_devices_label.set_sensitive(False) + + if not self.pref_dialog_startup: + logger.debug("Resetting sample file photo and video files") + self._setup_sample_names(use_dummy_data = True) + self._setup_photo_original_name() + self.update_photo_download_folder_example() + self.update_photo_rename_example() + self.update_video_download_folder_example() + self._setup_video_original_name() + self.update_video_rename_example() def update_misc_controls(self): """ diff --git a/rapid/prefsrapid.py b/rapid/prefsrapid.py index 82079ec..491d75d 100644 --- a/rapid/prefsrapid.py +++ b/rapid/prefsrapid.py @@ -165,6 +165,15 @@ class RapidPreferences(prefs.Preferences): return True return False + def must_synchronize_raw_jpg(self): + """Returns True if synchronize_raw_jpg is True and photo renaming + uses sequence values""" + if self.synchronize_raw_jpg: + for s in LIST_SEQUENCE_L1: + if self._pref_list_uses_component(self.image_rename, s, 1): + return True + return False + def any_pref_uses_stored_sequence_no(self): """Returns True if any of the pref lists contain a stored sequence no""" for pref_list in self._get_pref_lists(): diff --git a/rapid/rapid.py b/rapid/rapid.py index efd8145..3603025 100755 --- a/rapid/rapid.py +++ b/rapid/rapid.py @@ -57,6 +57,8 @@ import prefsrapid import tableplusminus as tpm import generatename as gn +import downloadtracker + from metadatavideo import DOWNLOAD_VIDEO import metadataphoto import metadatavideo @@ -65,6 +67,8 @@ import scan as scan_process import copyfiles import subfolderfile +import errorlog + import device as dv import utilities @@ -105,13 +109,6 @@ def date_time_human_readable(date, with_line_break=True): else: return _("%(date)s %(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")} -def time_subseconds_human_readable(date, subseconds): - return _("%(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \ - {'hour':date.strftime("%H"), - 'minute':date.strftime("%M"), - 'second':date.strftime("%S"), - 'subsecond': subseconds} - def date_time_subseconds_human_readable(date, subseconds): return _("%(date)s %(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \ {'date':date.strftime("%x"), @@ -228,9 +225,9 @@ class DeviceCollection(gtk.TreeView): else: return None - def update_progress(self, process_id, percent_complete, progress_bar_text, bytes_downloaded): + def update_progress(self, scan_pid, percent_complete, progress_bar_text, bytes_downloaded): - iter = self._get_process_map(process_id) + iter = self._get_process_map(scan_pid) if iter: if percent_complete: self.liststore.set_value(iter, 3, percent_complete) @@ -558,6 +555,16 @@ class ThumbnailDisplay(gtk.IconView): self.total_files += 1 + def get_sample_file(self, file_type): + """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) @@ -862,6 +869,7 @@ class TaskManager: def add_task(self, task): pid = self._setup_task(task) + logger.debug("TaskManager PID: %s", pid) return pid @@ -1032,6 +1040,7 @@ class SubfolderFileManager(SingleInstanceTaskManager): SingleInstanceTaskManager.__init__(self, results_callback) 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, download_count, rpd_file): @@ -1235,7 +1244,8 @@ class RapidApp(dbus.service.Object): # FIXME: need more fine grained tuning here - must cancel large file # copies midstream - logger.info("Terminating...") + if terminate_file_copies: + logger.info("Terminating all processes...") scan_termination_requested = self.scan_manager.request_termination() thumbnails_termination_requested = self.thumbnails.thumbnail_manager.request_termination() @@ -1760,11 +1770,7 @@ class RapidApp(dbus.service.Object): # Track download sizes and other values for each device. # (Scan id acts as an index to each device. A device could be scanned # more than once). - self.size_of_download_in_bytes_by_scan_pid = dict() - self.no_files_in_download_by_scan_pid = dict() - self.file_types_present_by_scan_pid = dict() - self.download_count_for_file_by_unique_id = dict() - self.download_count_by_scan_pid = dict() + self.download_tracker = downloadtracker.DownloadTracker() # Track which temporary directories are created when downloading files self.temp_dirs_by_scan_pid = dict() @@ -1781,17 +1787,23 @@ class RapidApp(dbus.service.Object): self.download_start_time = datetime.datetime.now() files_by_scan_pid = self.thumbnails.get_files_checked_for_download() - folders_valid = self.check_download_folder_validity(files_by_scan_pid) - - #FIXME: if invalid, display some kind of error message to the user + folders_valid, invalid_dirs = self.check_download_folder_validity(files_by_scan_pid) - if folders_valid: + if not folders_valid: + if len(invalid_dirs) > 1: + msg = _("These download folders are invalid:\n%(folder1)s\n%(folder2)s") % { + 'folder1': invalid_dirs[0], 'folder2': invalid_dirs[1]} + else: + msg = _("This download folder is invalid:\n%s") % invalid_dirs[0] + self.log_error(config.CRITICAL_ERROR, _("Download cannot proceed"), + msg) + else: self.thumbnails.mark_download_pending(files_by_scan_pid) for scan_pid in files_by_scan_pid: files = files_by_scan_pid[scan_pid] self.download_files(files, scan_pid) - self.set_download_action_label(is_download = False) + self.set_download_action_label(is_download = False) def pause_download(self): @@ -1820,8 +1832,10 @@ class RapidApp(dbus.service.Object): else: video_download_folder = None - self.size_of_download_in_bytes_by_scan_pid[scan_pid] = self.size_files_to_be_downloaded(files) - self.no_files_in_download_by_scan_pid[scan_pid] = len(files) + self.download_tracker.init_stats(scan_pid=scan_pid, + bytes=self.size_files_to_be_downloaded(files), + no_files=len(files)) + self.download_active_by_scan_pid.append(scan_pid) # Initiate copy files process self.copy_files_manager.add_task((photo_download_folder, @@ -1843,16 +1857,18 @@ class RapidApp(dbus.service.Object): self.temp_dirs_by_scan_pid[scan_pid] = (photo_temp_dir, video_temp_dir) elif msg_type == rpdmp.MSG_BYTES: scan_pid, total_downloaded = data - percent_complete = (float(total_downloaded) / - self.size_of_download_in_bytes_by_scan_pid[scan_pid]) * 100 + self.download_tracker.set_total_bytes_copied(scan_pid, + total_downloaded) + percent_complete = self.download_tracker.get_percent_complete(scan_pid) self.device_collection.update_progress(scan_pid, percent_complete, None, None) elif msg_type == rpdmp.MSG_FILE: download_succeeded, rpd_file, download_count, temp_full_file_name = data - - self.download_count_for_file_by_unique_id[rpd_file.unique_id] = download_count - self.download_count_by_scan_pid[rpd_file.scan_pid] = download_count + 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: @@ -1860,9 +1876,8 @@ class RapidApp(dbus.service.Object): rpd_file = prefsrapid.insert_pref_lists(self.prefs, rpd_file) rpd_file.strip_characters = self.prefs.strip_characters rpd_file.download_folder = self.prefs.get_download_folder_for_file_type(rpd_file.file_type) - - #~ if not download_succeeded: - #~ logger.error("File was not downloaded: %s", rpd_file.full_file_name) + rpd_file.download_conflict_resolution = self.prefs.download_conflict_resolution + rpd_file.synchronize_raw_jpg = self.prefs.must_synchronize_raw_jpg() self.subfolder_file_manager.rename_file_and_move_to_subfolder( download_succeeded, @@ -1896,17 +1911,25 @@ class RapidApp(dbus.service.Object): scan_pid = rpd_file.scan_pid unique_id = rpd_file.unique_id - self._update_file_download_device_progress(scan_pid, unique_id) - self.thumbnails.update_status_post_download(rpd_file) - download_count = self.download_count_for_file_by_unique_id[unique_id] - if download_count == self.no_files_in_download_by_scan_pid[scan_pid]: + # Update error log window if neccessary + if not move_succeeded: + self.log_error(config.SERIOUS_ERROR, rpd_file.error_title, + rpd_file.error_msg, rpd_file.error_extra_detail) + elif rpd_file.status == config.STATUS_DOWNLOADED_WITH_WARNING: + self.log_error(config.WARNING, rpd_file.error_title, + rpd_file.error_msg, rpd_file.error_extra_detail) + + self.download_tracker.file_downloaded_increment(scan_pid) + self._update_file_download_device_progress(scan_pid, unique_id) + + download_count = self.download_tracker.get_download_count_for_file(unique_id) + if download_count == self.download_tracker.get_no_files_in_download(scan_pid): # Last file has been downloaded, so clean temp directory logger.debug("Purging temp directories") self._clean_temp_dirs_for_scan_pid(scan_pid) - del self.no_files_in_download_by_scan_pid[scan_pid] - del self.size_of_download_in_bytes_by_scan_pid[scan_pid] + self.download_tracker.purge(scan_pid) self.download_active_by_scan_pid.remove(scan_pid) if not self.download_is_occurring(): @@ -1931,13 +1954,15 @@ class RapidApp(dbus.service.Object): """ Increments the progress bar for an individual device """ - #~ scan_pid = rpd_file.scan_pid - #~ unique_id = rpd_file.unique_id progress_bar_text = _("%(number)s of %(total)s %(filetypes)s") % \ - {'number': self.download_count_for_file_by_unique_id[unique_id], - 'total': self.no_files_in_download_by_scan_pid[scan_pid], - 'filetypes': self.file_types_present_by_scan_pid[scan_pid]} - self.device_collection.update_progress(scan_pid, None, progress_bar_text, None) + {'number': self.download_tracker.get_download_count_for_file(unique_id), + 'total': self.download_tracker.get_no_files_in_download(scan_pid), + 'filetypes': self.download_tracker.get_file_types_present(scan_pid)} + 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, + bytes_downloaded=None) def _clean_all_temp_dirs(self): """ @@ -1998,7 +2023,7 @@ class RapidApp(dbus.service.Object): self.prefs.video_rename, self.prefs.video_subfolder) if not prefs_ok: - logger.error("There is an error in the program preferences. Some preferences will be reset.") + logger.error("There is an error in the program preferences relating to file renaming and subfolder creation. Some preferences will be reset.") return prefs_ok @@ -2096,6 +2121,8 @@ class RapidApp(dbus.service.Object): self.thumbnails.clear_all() self.setup_devices(on_startup = False, on_preference_change = True, do_not_allow_auto_start = True) + 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 @@ -2139,6 +2166,7 @@ class RapidApp(dbus.service.Object): self.device_collection_scrolledwindow = builder.get_object("device_collection_scrolledwindow") self.next_image_action = builder.get_object("next_image_action") self.prev_image_action = builder.get_object("prev_image_action") + self.menu_log_window = builder.get_object("menu_log_window") # Only enable this action when actually displaying a preview self.next_image_action.set_sensitive(False) @@ -2161,6 +2189,9 @@ class RapidApp(dbus.service.Object): 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 @@ -2258,8 +2289,8 @@ class RapidApp(dbus.service.Object): Also displays backup volumes / path being used. (NOT IMPLEMENTED YET) """ - photo_dir = self.is_valid_download_dir(self.prefs.download_folder) - video_dir = self.is_valid_download_dir(self.prefs.video_download_folder) + 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) if photo_dir and video_dir: same_file_system = self.same_file_system(self.prefs.download_folder, self.prefs.video_download_folder) @@ -2272,6 +2303,7 @@ class RapidApp(dbus.service.Object): if video_dir and not same_file_system: dirs.append((self.prefs.video_download_folder, _("videos"))) + msg = '' if len(dirs) > 1: msg = ' ' + _('Free space:') + ' ' @@ -2313,7 +2345,7 @@ class RapidApp(dbus.service.Object): # user manually specified backup location msg2 = _('Backing up to %(path)s') % {'path':self.prefs.backup_location} else: - msg2 = self.displayBackupVolumes() + msg2 = self.displayBackupVolumes() #FIXME if msg: msg = _("%(freespace)s. %(backuppaths)s.") % {'freespace': msg, 'backuppaths': msg2} @@ -2323,6 +2355,27 @@ class RapidApp(dbus.service.Object): 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() + + + def on_menu_log_window_toggled(self, widget): + active = widget.get_active() + self.prefs.show_log_dialog = active + if active: + self.error_log.widget.show() + else: + self.error_log.widget.hide() + # # # # Utility functions @@ -2352,8 +2405,12 @@ class RapidApp(dbus.service.Object): """ 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. """ valid = True + invalid_dirs = [] # first, check what needs to be downloaded - photos and / or videos need_photo_folder = False need_video_folder = False @@ -2369,14 +2426,18 @@ class RapidApp(dbus.service.Object): # 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) if need_video_folder: - if not self.is_valid_download_dir(self.prefs.video_download_folder): + if not self.is_valid_download_dir(self.prefs.video_download_folder, + is_photo_dir=False): valid = False + invalid_dirs.append(self.prefs.video_download_folder) - return valid + return (valid, invalid_dirs) def same_file_system(self, file1, file2): """Returns True if the files / diretories are on the same file system @@ -2403,24 +2464,47 @@ class RapidApp(dbus.service.Object): f2_id = f2_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILE) return f1_id == f2_id - def is_valid_download_dir(self, path): + 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 """ valid = False + if is_photo_dir: + download_folder_type = _("Photo") + else: + download_folder_type = _("Video") + try: d = gio.File(path) if not d.query_exists(cancellable=None): - logger.error("Download directory does not exist: %s", path) + logger.error("%s download folder does not exist: %s", + download_folder_type, path) + if show_error_in_log: + severity = config.WARNING + problem = _("%(file_type)s download folder does not exist") % { + 'file_type': download_folder_type} + details = _("Folder: %s") % path + self.log_error(severity, problem, details) else: 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 is an invalid directory", path) + 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) else: # is the directory writable? try: @@ -2428,6 +2512,12 @@ class RapidApp(dbus.service.Object): valid = True except: logger.error("%s is not writable", path) + if show_error_in_log: + severity = config.WARNING + problem = _("%(file_type)s download folder is not writable") % { + 'file_type': download_folder_type} + details = _("Folder: %s") % path + self.log_error(severity, problem, details) else: f = gio.File(temp_dir) f.delete(cancellable=None) @@ -2489,7 +2579,7 @@ class RapidApp(dbus.service.Object): size, file_type_counter, scan_pid = data size = format_size_for_user(bytes=size) results_summary, file_types_present = file_type_counter.summarize_file_count() - self.file_types_present_by_scan_pid[scan_pid] = file_types_present + self.download_tracker.set_file_types_present(scan_pid, file_types_present) logger.info('Found %s' % results_summary) logger.info('Files total %s' % size) self.device_collection.update_device(scan_pid, size) @@ -2508,7 +2598,7 @@ class RapidApp(dbus.service.Object): return False else: if len(data) > self.batch_size: - logger.error("incoming pipe length is %s" % len(data)) + logger.critical("incoming pipe length is unexpectedly long: %s" % len(data)) else: for rpd_file in data: self.thumbnails.add_file(rpd_file) diff --git a/rapid/rpdfile.py b/rapid/rpdfile.py index 03f3af9..a0969f0 100644 --- a/rapid/rpdfile.py +++ b/rapid/rpdfile.py @@ -20,6 +20,8 @@ import os import gtk +import time, datetime + import multiprocessing, logging logger = multiprocessing.get_logger() @@ -195,6 +197,9 @@ class RPDFile: def initialize_problem(self): self.problem = pn.Problem() + # these next values are used to display in the error log window + # the information in them can vary from other forms of display of errors + self.error_title = self.error_msg = self.error_extra_detail = '' def has_problem(self): if self.problem is None: @@ -245,7 +250,6 @@ class Photo(RPDFile): self.file_type = FILE_TYPE_PHOTO def load_metadata(self): - #~ self.exif_tags = [] self.metadata = metadataphoto.MetaData(self.full_file_name) try: @@ -255,11 +259,6 @@ class Photo(RPDFile): return False else: return True - - - #~ for tag in exif_tags_needed: - #~ if tag in metadata.exif_keys: - #~ self.exif_tags.append(metadata[tag]) class Video(RPDFile): @@ -273,5 +272,29 @@ class Video(RPDFile): def load_metadata(self): self.metadata = metadatavideo.VideoMetaData(self.full_file_name) return True - - + +class SamplePhoto(Photo): + def __init__(self, sample_name='IMG_0524.CR2', sequences=None): + Photo.__init__(self, name=sample_name, + display_name=sample_name, + path='/media/EOS_DIGITAL/DCIM/100EOS5D', + size=23516764, + file_system_modification_time=time.time(), + scan_pid=2033, + file_id='9873afe') + self.sequences = sequences + self.metadata = metadataphoto.DummyMetaData() + self.download_start_time = datetime.datetime.now() + +class SampleVideo(Video): + def __init__(self, sample_name='MVI_1379.MOV', sequences=None): + Video.__init__(self, name=sample_name, + display_name=sample_name, + path='/media/EOS_DIGITAL/DCIM/100EOS5D', + size=823513764, + file_system_modification_time=time.time(), + scan_pid=2033, + file_id='9873qrsfe') + self.sequences = sequences + self.metadata = metadatavideo.DummyMetaData(filename=sample_name) + self.download_start_time = datetime.datetime.now() diff --git a/rapid/scan.py b/rapid/scan.py index ce882b4..e55f43d 100755 --- a/rapid/scan.py +++ b/rapid/scan.py @@ -93,7 +93,7 @@ class Scan(multiprocessing.Process): if not self.terminate_queue.empty(): x = self.terminate_queue.get() # terminate immediately - print "terminating..." + logger.info("terminating scan...") self.files = [] return None diff --git a/rapid/subfolderfile.py b/rapid/subfolderfile.py index 53118c2..c55b6d8 100644 --- a/rapid/subfolderfile.py +++ b/rapid/subfolderfile.py @@ -23,7 +23,7 @@ Generates names for files and folders. Runs a daemon process. """ -import os +import os, datetime, collections import gio import multiprocessing @@ -40,17 +40,66 @@ import config 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) + else: + 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]: + return (-1, self.photos[name][3]) + else: + return (0, self.photos[name][3]) + 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"), + 'second':date.strftime("%S"), + 'subsecond': subseconds} +def load_metadata(rpd_file): + """ + Loads the metadata for the file. Returns True if operation succeeded, false + otherwise + """ + if rpd_file.metadata is None: + if not rpd_file.load_metadata(): + # 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: - if rpd_file.metadata is None: - if not rpd_file.load_metadata(): - # Error in reading metadata - rpd_file.add_problem(None, pn.CANNOT_DOWNLOAD_BAD_METADATA, {'filetype': rpd_file.title_capitalized}) - do_generation = False + do_generation = load_metadata(rpd_file) else: if rpd_file.metadata is None: rpd_file.load_metadata() @@ -103,12 +152,85 @@ class SubfolderFile(multiprocessing.Process): logger.debug("Start of day is set to %s", self.day_start.value) - - - def progress_callback_no_update(self, amount_downloaded, total): pass + def file_exists(self, rpd_file, identifier=None): + """ + Notify user that the download file already exists + """ + # get information on when the existing file was last modified + try: + modification_time = os.path.getmtime(rpd_file.download_full_file_name) + dt = datetime.datetime.fromtimestamp(modification_time) + date = dt.strftime("%x") + time = dt.strftime("%X") + except: + 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, + {'filetype':rpd_file.title_capitalized}) + 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, + {'filetype':rpd_file.title_capitalized}) + 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 + rpd_file.error_extra_detail = pn.extra_detail_definitions[pn.UNIQUE_IDENTIFIER] % \ + {'identifier': identifier, 'filetype': rpd_file.title, + '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, + '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 + """ + rpd_file.add_problem(None, pn.DOWNLOAD_COPYING_ERROR, {'filetype': rpd_file.title}) + 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 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), + 'image1_date': i1_date_time.strftime("%x"), + '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.sub_seconds())} + rpd_file.add_problem(None, pn.SAME_FILE_DIFFERENT_EXIF, detail) + + rpd_file.error_title = _('Photos detected with the same filenames, but taken at different times') + 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. @@ -118,6 +240,8 @@ class SubfolderFile(multiprocessing.Process): """ i = 0 download_count = 0 + + duplicate_files = {} # Track downloads today, using a class whose purpose is to @@ -133,6 +257,8 @@ class SubfolderFile(multiprocessing.Process): 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) @@ -148,42 +274,79 @@ class SubfolderFile(multiprocessing.Process): if download_succeeded: temp_file = gio.File(rpd_file.temp_full_file_name) - # Generate subfolder name and new file name - generation_succeeded = True - rpd_file = generate_subfolder(rpd_file) - if rpd_file.download_subfolder: + synchronize_raw_jpg_failed = False + if not (rpd_file.synchronize_raw_jpg and + rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO): + synchronize_raw_jpg = False + sequence_to_use = None + else: + synchronize_raw_jpg = True + sync_photo_name, sync_photo_ext = os.path.splitext(rpd_file.name) + if not load_metadata(rpd_file): + 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(), + 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) + if (rpd_file.download_conflict_resolution <> + config.ADD_UNIQUE_IDENTIFIER): + rpd_file.add_problem(None, pn.FILE_ALREADY_DOWNLOADED, {'filetype': rpd_file.title_capitalized}) + rpd_file.error_title = _('Photo has already been downloaded') + rpd_file.error_msg = _("Source: %(source)s") % {'source': rpd_file.full_file_name} + rpd_file.status = config.STATUS_DOWNLOAD_FAILED + synchronize_raw_jpg_failed = True + else: + 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 self.refresh_downloads_today.value: - # overwrite downloads today value tracked here, - # as user has modified their preferences - self.downloads_today_tracker.set_raw_downloads_today_from_int(self.downloads_today.value) - 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 + if synchronize_raw_jpg_failed: + generation_succeeded = False + else: + # Generate subfolder name and new file name + generation_succeeded = True + rpd_file = generate_subfolder(rpd_file) + if rpd_file.download_subfolder: - # 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(): - rpd_file.status = config.STATUS_DOWNLOADED_WITH_WARNING + if self.refresh_downloads_today.value: + # overwrite downloads today value tracked here, + # as user has modified their preferences + self.downloads_today_tracker.set_raw_downloads_today_from_int(self.downloads_today.value) + 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(): + rpd_file.status = config.STATUS_DOWNLOADED_WITH_WARNING - # 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: - area = _("subfolder and filename") - elif not rpd_file.download_name: - area = _("filename") - else: - area = _("subfolder") - rpd_file.add_problem(None, pn.ERROR_IN_NAME_GENERATION, {'filetype': rpd_file.title_capitalized, 'area': area}) - rpd_file.add_extra_detail(pn.NO_DATA_TO_NAME, {'filetype': area}) - generation_succeeded = False - rpd_file.status = config.STATUS_DOWNLOAD_FAILED - # FIXME: log error + # 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: + area = _("subfolder and filename") + elif not rpd_file.download_name: + area = _("filename") + else: + area = _("subfolder") + rpd_file.add_problem(None, pn.ERROR_IN_NAME_GENERATION, {'filetype': rpd_file.title_capitalized, 'area': area}) + 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) @@ -205,36 +368,83 @@ class SubfolderFile(multiprocessing.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 directory: %s", rpd_file.download_path) + 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 # 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: - rpd_file.add_problem(None, pn.DOWNLOAD_COPYING_ERROR, {'filetype': rpd_file.title}) - 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) + if inst.code == gio.ERROR_EXISTS: + if (rpd_file.download_conflict_resolution == + config.ADD_UNIQUE_IDENTIFIER): + add_unique_identifier = True + else: + rpd_file = self.file_exists(rpd_file) + else: + rpd_file = self.download_failure_file_error(rpd_file, inst) + + if add_unique_identifier: + name = os.path.splitext(rpd_file.download_name) + full_name = rpd_file.download_full_file_name + suffix_already_used = True + while suffix_already_used: + duplicate_files[full_name] = duplicate_files.get( + full_name, 0) + 1 + identifier = '_%s' % duplicate_files[full_name] + rpd_file.download_name = name[0] + identifier + name[1] + rpd_file.download_full_file_name = os.path.join( + rpd_file.download_path, + rpd_file.download_name) + 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: + rpd_file = self.download_failure_file_error(rpd_file, inst) + + logger.debug("Finish processing file: %s", download_count) if move_succeeded: - if self.uses_session_sequece_no.value or self.uses_sequence_letter.value: - self.sequences.increment( - 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 synchronize_raw_jpg: + if sequence_to_use is None: + sequence = self.sequences.create_matched_sequences() + else: + sequence = sequence_to_use + self.sync_raw_jpeg.add_download(name=sync_photo_name, + extension=sync_photo_ext, + date_time=rpd_file.metadata.date_time(), + sub_seconds=rpd_file.metadata.sub_seconds(), + sequence_number_used=sequence) + 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_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 not move_succeeded: logger.error("%s: %s - %s", rpd_file.full_file_name, @@ -251,6 +461,7 @@ class SubfolderFile(multiprocessing.Process): rpd_file.metadata = None #purge metadata, as it cannot be pickled + rpd_file.sequences = None self.results_pipe.send((move_succeeded, rpd_file,)) i += 1 |