diff options
author | Julien Valroff <julien@kirya.net> | 2013-02-15 18:54:39 +0100 |
---|---|---|
committer | Julien Valroff <julien@kirya.net> | 2013-02-15 18:54:39 +0100 |
commit | 12f73b3fc8d8d36fbee892305bdf772854b90bfe (patch) | |
tree | 670a0fbfd0b5835d25c17baa4dbccbb7e5b1ace5 /rapid/rapid.py | |
parent | 1e318005d23d0091cd22191f25881b4ac9ff78a7 (diff) | |
parent | 063aca3a12dbf69d5ecdd2e949a788e01c91659d (diff) |
Merge tag 'upstream/0.4.6'
Upstream version 0.4.6
Diffstat (limited to 'rapid/rapid.py')
-rwxr-xr-x | rapid/rapid.py | 1771 |
1 files changed, 902 insertions, 869 deletions
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() |