diff options
Diffstat (limited to 'rapid/rapid.py')
-rwxr-xr-x | rapid/rapid.py | 4177 |
1 files changed, 0 insertions, 4177 deletions
diff --git a/rapid/rapid.py b/rapid/rapid.py deleted file mode 100755 index 303e1f7..0000000 --- a/rapid/rapid.py +++ /dev/null @@ -1,4177 +0,0 @@ -#!/usr/bin/python -# -*- coding: latin1 -*- - -### Copyright (C) 2011-2014 Damon Lynch <damonlynch@gmail.com> - -### This program is free software; you can redistribute it and/or modify -### it under the terms of the GNU General Public License as published by -### the Free Software Foundation; either version 2 of the License, or -### (at your option) any later version. - -### This program is distributed in the hope that it will be useful, -### but WITHOUT ANY WARRANTY; without even the implied warranty of -### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -### GNU General Public License for more details. - -### You should have received a copy of the GNU General Public License -### along with this program; if not, write to the Free Software -### Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 -### USA - -use_pynotify = True - -import tempfile - -import dbus -import dbus.bus -import dbus.service -from dbus.mainloop.glib import DBusGMainLoop -DBusGMainLoop(set_as_default=True) - -from optparse import OptionParser - -import gtk -import gtk.gdk as gdk - -from gobject.constants import G_MAXINT - -import webbrowser - -import sys, time, types, os, datetime - -import gobject, pango, cairo, array, pangocairo, gio - -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 - -import logging -logger = log_to_stderr() - -# Rapid Photo Downloader modules - -import rpdfile - -from misc import get_folder_selection - -import problemnotification as pn -import thumbnail as tn -import rpdmultiprocessing as rpdmp - -import preferencesdialog -import prefsrapid - -import tableplusminus as tpm -import generatename as gn - -import downloadtracker - -import filemodify - -from metadatavideo import DOWNLOAD_VIDEO, file_types_to_download -import metadataphoto -import metadatavideo -import metadataexiftool - -import scan as scan_process -import copyfiles -import subfolderfile -import backupfile -from backupfile import PHOTO_BACKUP, VIDEO_BACKUP, PHOTO_VIDEO_BACKUP - -import errorlog - -import device as dv -import utilities - -import config -__version__ = config.version - -import paths - -import gettext -gettext.bindtextdomain(config.APP_NAME) -gettext.textdomain(config.APP_NAME) - -_ = gettext.gettext - - -from utilities import format_size_for_user -from utilities import register_iconsets - - -from config import STATUS_CANNOT_DOWNLOAD, STATUS_DOWNLOADED, \ - STATUS_DOWNLOADED_WITH_WARNING, \ - STATUS_DOWNLOAD_FAILED, \ - STATUS_DOWNLOAD_PENDING, \ - STATUS_BACKUP_PROBLEM, \ - 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 -PROGRAM_NAME = _('Rapid Photo Downloader') -__version__ = config.version - -def date_time_human_readable(date, with_line_break=True): - if with_line_break: - 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"), - 'hour':date.strftime("%H"), - 'minute':date.strftime("%M"), - 'second':date.strftime("%S"), - 'subsecond': subseconds} - - -class DeviceCollection(gtk.TreeView): - """ - TreeView display of devices and how many files have been copied, shown - immediately under the menu in the main application window. - """ - def __init__(self, parent_app): - - self.parent_app = parent_app - # 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, - gtk.gdk.Pixbuf, int, int) - self.map_process_to_row = {} - 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, - # external hard drive, Portable Storage Device, etc. - column0 = gtk.TreeViewColumn(_("Device")) - pixbuf_renderer = gtk.CellRendererPixbuf() - pixbuf_renderer.set_padding(2, 0) - text_renderer = gtk.CellRendererText() - text_renderer.props.ellipsize = pango.ELLIPSIZE_MIDDLE - text_renderer.set_fixed_size(160, -1) - eject_renderer = gtk.CellRendererPixbuf() - column0.pack_start(pixbuf_renderer, expand=False) - column0.pack_start(text_renderer, expand=True) - column0.pack_end(eject_renderer, expand=False) - column0.add_attribute(pixbuf_renderer, 'pixbuf', 0) - 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"), - 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, - 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, - progress, - progress_bar_text, - 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 - # e.g. when starting with 3 cards, it could be 18, but when adding 2 cards to the already running program - # (with one card at startup), it could be 21 - # must account for header row at the top - row_height = self.get_background_area(0, self.get_column(0))[3] + 1 - height = 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 - """ - if process_id in self.map_process_to_row: - iter = self._get_process_map(process_id) - 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 - """ - 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 - 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() - iter = self.liststore.get_iter(path) - 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 - self.liststore.set_value(iter, 7, self.liststore.get_value(iter, 7) + 1) - else: - # Set to finished state - self.liststore.set_value(iter, 7, G_MAXINT) - else: - # Reset to allow fraction to be set - self.liststore.set_value(iter, 7, -1) - - - - def button_clicked(self, widget, event): - """ - Look for left single click on eject button - """ - if event.button == 1: - if len (self.liststore): - x = int(event.x) - y = int(event.y) - path, column, cell_x, cell_y = self.get_path_at_pos(x, y) - if path is not None: - if column == self.get_column(0): - if cell_x >= column.get_width() - self.eject_pixbuf.get_width(): - 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() - - try: - mount.unmount_finish(result) - logger.debug("%s successfully unmounted" % name) - except gio.Error, inst: - logger.error("%s did not unmount: %s", name, inst) - - 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) - try: - n.show() - except: - logger.error("Unable to display message using notification system") - - -def create_cairo_image_surface(pil_image, image_width, image_height): - imgd = pil_image.tobytes("raw","BGRA", 0, 1) - data = array.array('B',imgd) - stride = image_width * 4 - image = cairo.ImageSurface.create_for_data(data, cairo.FORMAT_ARGB32, - image_width, image_height, stride) - return image - -class ThumbnailCellRenderer(gtk.CellRenderer): - __gproperties__ = { - "image": (gobject.TYPE_PYOBJECT, "Image", - "Image", gobject.PARAM_READWRITE), - - "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 - #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: - image_x = x + ((w - image_w) / 2) - image_y = y + self.image_area_size - image_h - - #convert PIL image to format suitable for cairo - image = create_cairo_image_surface(self.image, image_w, image_h) - - # draw a light grey border of 1px around the image - cairo_context.set_source_rgb(0.66, 0.66, 0.66) #light grey, #a9a9a9 - 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() - - 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 - font_family = pango.AttrFamily('sans', 0, -1) - attr = pango.AttrList() - attr.insert(fg_color) - attr.insert(font_size) - attr.insert(font_family) - layout.set_attributes(attr) - - layout.set_text(self.filename) - - context.move_to(text_x, text_y) - context.show_layout(layout) - - #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): - gtk.IconView.__init__(self) - 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.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 - self.FILETYPE_COL = 5 - 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 - checkbutton.props.xalign = 0.0 - checkbutton.connect('toggled', self.on_checkbutton_toggled) - self.pack_end(checkbutton, expand=False) - - 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) - self.add_attribute(image, "filename", 3) - self.add_attribute(image, "status", 8) - - #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 - """ - self.liststore = gtk.ListStore( - gobject.TYPE_PYOBJECT, # 0 PIL thumbnail - gobject.TYPE_BOOLEAN, # 1 selected or not - str, # 2 unique id - str, # 3 file name - int, # 4 timestamp for sorting, converted float - int, # 5 file type i.e. photo or video - 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 - - size = 16 - # standard icons - failed = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU) - self.download_failed_icon = failed.scale_simple(size, size, gtk.gdk.INTERP_HYPER) - error = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU) - self.error_icon = error.scale_simple(size, size, gtk.gdk.INTERP_HYPER) - warning = self.render_icon(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_MENU) - self.warning_icon = warning.scale_simple(size, size, gtk.gdk.INTERP_HYPER) - - # Rapid Photo Downloader specific icons - self.downloaded_icon = gtk.gdk.pixbuf_new_from_file_at_size( - paths.share_dir('glade3/rapid-photo-downloader-downloaded.svg'), - 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) - 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 - """ - if status == STATUS_WARNING: - status_icon = self.warning_icon - elif status == STATUS_CANNOT_DOWNLOAD: - status_icon = self.error_icon - elif status == STATUS_DOWNLOADED: - status_icon = self.downloaded_icon - elif status == STATUS_NOT_DOWNLOADED: - status_icon = self.not_downloaded_icon - elif status in [STATUS_DOWNLOADED_WITH_WARNING, STATUS_BACKUP_PROBLEM]: - status_icon = self.downloaded_with_warning_icon - elif status in [STATUS_DOWNLOAD_FAILED, STATUS_DOWNLOAD_AND_BACKUP_FAILED]: - status_icon = self.downloaded_with_error_icon - elif status == STATUS_DOWNLOAD_PENDING: - status_icon = self.download_pending_icon - else: - logger.critical("FIXME: unknown status: %s", status) - status_icon = self.not_downloaded_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): - """ - Allow selections by row (and not GTK default by square) when user is - dragging the mouse or using the keyboard to select - """ - selections = self.get_selected_items() - if len(selections) > 1: - previous_sel = selections[0][0] + 1 - # seleted items list always starts with the highest selected item - for selection in selections: - current_sel = selection[0] - if current_sel <> (previous_sel-1): - for i in range(previous_sel-1, current_sel, -1): - self.select_path(i) - previous_sel = current_sel - 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) - unique_id = rpd_file.unique_id - scan_pid = rpd_file.scan_pid - - timestamp = int(rpd_file.modification_time) - - iter = self.liststore.append((thumbnail_icon, - True, - unique_id, - rpd_file.display_name, - timestamp, - rpd_file.file_type, - True, - 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 - 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): - """ - """ - 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 - if rpd_file.status in DOWNLOADED: - file_location = rpd_file.download_full_file_name - thm_file_name = rpd_file.download_thm_full_name - else: - file_location = rpd_file.full_file_name - thm_file_name = rpd_file.thm_full_name - - self.preview_manager.get_preview(unique_id, file_location, - thm_file_name, - rpd_file.file_type, size_max=None,) - - self.previews_being_fetched.add(unique_id) - - def show_preview(self, unique_id=None, iter=None): - if len(self.liststore): - if unique_id is not None: - iter = self.get_iter_from_unique_id(unique_id) - elif iter is not None: - unique_id = self.get_unique_id_from_iter(iter) - else: - # neither an iter or a unique_id were passed - # use iter from first selected file - # if none is selected, choose the first file - selected = self.get_selected_items() - if selected: - path = selected[0] - else: - path = 0 - iter = self.liststore.get_iter(path) - unique_id = self.get_unique_id_from_iter(iter) - - - 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: - 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, - 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: - row = len(self.liststore)-1 - else: - row -= 1 - iter = self.liststore.get_iter(row) - 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]: - if file_type is not None: - if row[self.FILETYPE_COL] == file_type: - row[self.SELECTED_COL] = check_all - 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 - intend to download, else returns False. - """ - for row in self.liststore: - if row[self.SELECTED_COL]: - rpd_file = self.rpd_files[row[self.UNIQUE_ID_COL]] - 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() - if scan_pid is None: - for row in self.liststore: - if row[self.SELECTED_COL]: - rpd_file = self.rpd_files[row[self.UNIQUE_ID_COL]] - if rpd_file.status not in DOWNLOADED: - scan_pid = rpd_file.scan_pid - if scan_pid in files: - files[scan_pid].append(rpd_file) - else: - files[scan_pid] = [rpd_file,] - else: - files[scan_pid] = [] - for unique_id in self.process_index[scan_pid]: - rpd_file = self.rpd_files[unique_id] - if rpd_file.status not in DOWNLOADED: - iter = self.get_iter_from_unique_id(unique_id) - 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 - scan_pid - """ - i = 0 - for unique_id in self.process_index[scan_pid]: - rpd_file = self.rpd_files[unique_id] - 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 - 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): - """ - Sets status to download pending and updates thumbnails display - """ - for scan_pid in files_by_scan_pid: - for rpd_file in files_by_scan_pid[scan_pid]: - unique_id = rpd_file.unique_id - self.rpd_files[unique_id].status = STATUS_DOWNLOAD_PENDING - iter = self.get_iter_from_unique_id(unique_id) - if not self.rapid_app.auto_start_is_on: - # don't make the checkbox invisible immediately when on auto start - # otherwise the box can be rendred at the wrong size, as it is - # realized after the checkbox has already been made invisible - self.liststore.set_value(iter, self.CHECKBUTTON_VISIBLE_COL, False) - self.liststore.set_value(iter, self.SELECTED_COL, False) - 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) - icon = self.get_status_icon(rpd_file.status) - 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 - """ - if scan_pid in self.process_index: - rpd_files = [self.rpd_files[unique_id] for unique_id in self.process_index[scan_pid]] - thumbnail_pid = self.thumbnail_manager.add_task((scan_pid, rpd_files)) - self.generating_thumbnails[scan_pid] = thumbnail_pid - - def _set_thumbnail(self, unique_id, icon): - treerowref = self.treerow_index[unique_id] - path = treerowref.get_path() - iter = self.liststore.get_iter(path) - 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 - 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: - self._reset_thumbnail_tracking_and_display() - else: - 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 - """ - self.previews_being_fetched.remove(unique_id) - if preview_full_size: - 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") - self.set_model(None) - self._create_liststore() - - self.treerow_index = {} - self.process_index = {} - - self.rpd_files = {} - else: - if scan_pid in self.process_index: - for unique_id in self.process_index[scan_pid]: - rpd_file = self.rpd_files[unique_id] - if not keep_downloaded_files or not rpd_file.status in DOWNLOADED: - treerowref = self.treerow_index[rpd_file.unique_id] - path = treerowref.get_path() - iter = self.liststore.get_iter(path) - self.liststore.remove(iter) - del self.treerow_index[rpd_file.unique_id] - 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, - 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 - immediately terminate - """ - for p in self.processes(): - 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 - """ - requested = False - for p in self.processes(): - 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. - - 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, - 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 - """ - i = 0 - for p in self.processes(): - if p[0].is_alive(): - i += 1 - return i - - -class ScanManager(TaskManager): - - 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, - 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, - task_process_conn, terminate_queue, run_event) - scan.start() - self._processes.append((scan, terminate_queue, run_event)) - 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 - # (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, - task_process_conn, terminate_queue, run_event): - photo_download_folder = task[0] - video_download_folder = task[1] - scan_pid = task[2] - files = task[3] - verify_files = task[4] - modify_files_during_download = task[5] - modify_pipe = task[6] - - copy_files = copyfiles.CopyFiles(photo_download_folder, - video_download_folder, - files, - verify_files, - modify_files_during_download, - modify_pipe, - 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, - run_event) - generator.start() - self._processes.append((generator, terminate_queue, run_event)) - return generator.pid - -class FileModifyManager(TaskManager): - """Handles the modification or verification of of downloaded files before they are renamed - Duplex, multiprocess, similar to BackupFilesManager - """ - def __init__(self, results_callback): - TaskManager.__init__(self, results_callback=results_callback, - batch_size=0) - self.file_modify_by_scan_pid = {} - - def _initiate_task(self, task, task_results_conn, task_process_conn, - terminate_queue, run_event): - scan_pid = task[0] - auto_rotate_jpeg = task[1] - focal_length = task[2] - verify_file = task[3] - refresh_md5_on_file_change = task[4] - - file_modify = filemodify.FileModify(auto_rotate_jpeg, focal_length, - verify_file, refresh_md5_on_file_change, - task_process_conn, terminate_queue, - run_event) - file_modify.start() - self._processes.append((file_modify, terminate_queue, run_event, - task_results_conn)) - - self.file_modify_by_scan_pid[scan_pid] = (task_results_conn, file_modify.pid) - - return file_modify.pid - - def _setup_pipe(self): - return Pipe(duplex=True) - - def _send_termination_msg(self, p): - p[1].put(None) - p[3].send((None, None)) - - def get_modify_pipe(self, scan_pid): - return self.file_modify_by_scan_pid[scan_pid][0] - - -class BackupFilesManager(TaskManager): - """ - Handles backup processes. This is a little different from some other Task - Manager classes in that its pipe is Duplex, and the work done by it - is not pre-assigned when the process is started. - - Duplex, multiprocess. - """ - def __init__(self, results_callback, batch_size): - TaskManager.__init__(self, results_callback, batch_size) - self.backup_devices_by_path = {} - - 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, 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, - run_event) - backup_files.start() - 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, - verify_file, - download_count): - - if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: - logger.debug("Backing up photo %s", rpd_file.download_name) - else: - logger.debug("Backing up video %s", rpd_file.download_name) - - for path in self.backup_devices_by_path: - backup_type = self.backup_devices_by_path[path][2] - do_backup = ((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)) - if do_backup: - logger.debug("Backing up to %s", path) - else: - logger.debug("Not backing up to %s", path) - # Even if not going to backup to this device, need to send it anyway so - # progress bar can be updated. Not this most efficient but the - # code is much more simple - task_results_conn = self.backup_devices_by_path[path][0] - task_results_conn.send((move_succeeded, do_backup, rpd_file, - path_suffix, - backup_duplicate_overwrite, - verify_file, download_count)) - - 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): - 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 - -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, - sequence_values) - self._subfolder_file.start() - logger.debug("SubfolderFile PID: %s", self._subfolder_file.pid) - - def rename_file_and_move_to_subfolder(self, download_succeeded, - download_count, rpd_file): - - logger.debug("Sending file for rename: %s.", download_count) - self.task_results_conn.send((download_succeeded, download_count, - rpd_file)) - - - def task_results(self, source, condition): - 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 - w = event.area.width - h = event.area.height - - #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() - - 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 - width = int(height * self.base_image_aspect) - else: - # 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: - logger.debug("Upsizing image") - pil_image = tn.upsize_pil(pil_image, (width, height)) - else: - logger.debug("Downsizing image") - tn.downsize_pil(pil_image, (width, height)) - - #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() - - 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_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, - checked=None): - """ - """ - self.preview_image.set_image(pil_image) - self.unique_id = unique_id - if checked is not None: - self.download_this_checkbutton.set_active(checked) - self.download_this_checkbutton.grab_focus() - - 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): - - 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() - - 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, - 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) - - # save window and component sizes - self.prefs.vpaned_pos = self.main_vpaned.get_position() - - 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() - 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 - 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): - time.sleep(1) - # must try again, just in case a new scan has meanwhile started! - self.scan_manager.request_termination() - self.thumbnails.thumbnail_manager.terminate_forcefully() - self.scan_manager.terminate_forcefully() - self.backup_manager.terminate_forcefully() - self.file_modify_manager.terminate_forcefully() - - if terminate_file_copies and copy_files_termination_requested: - time.sleep(1) - 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 - # # # - - def on_download_this_checkbutton_toggled(self, checkbutton): - 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() - - 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, - file_type=rpdfile.FILE_TYPE_PHOTO) - - def on_check_all_videos_action_activate(self, action): - 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") - self.main_notebook.set_current_page(1) - 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: - 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 - thumbnails in the icon view - """ - if self.scan_manager.no_tasks == 0: - self.thumbnails.sort_by_timestamp() - self.thumbnails.display_thumbnails() - - - # # # - # 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) - - - 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 - any paths to ignore while scanning - """ - 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 - that could take a very long time to scan. They can choose yes or no. - - Returns True if yes or there was no need to ask the user, False if the - user said no. - """ - l = self.prefs.device_location - if l in ['/media', '/run', os.path.expanduser('~'), '/']: - logger.info("Prompting whether to download from %s", l) - if l == '/': - #this location is a human readable explanation for /, and is inserted into Downloading from %(location)s - l = _('the root of the file system') - c = preferencesdialog.QuestionDialog(parent_window=self.rapidapp, - title=_('Rapid Photo Downloader'), - #message in dialog box which asks the user if they really want to be downloading from this location - question="<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) - 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, - 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 - """ - - if self.using_volume_monitor(): - self.start_volume_monitor() - - self.clear_non_running_downloads() - if not self.prefs.device_autodetection: - if not self.confirm_manual_location(): - return - - mounts = [] - self.backup_devices = {} - - if self.using_volume_monitor(): - # either using automatically detected backup devices - # or download devices - for mount in self.vmonitor.get_mounts(): - if not mount.is_shadowed(): - path = mount.get_root().get_path() - if path: - if (path in self.prefs.device_blacklist and - self.search_for_PSD()): - logger.info("blacklisted device %s ignored", mount.get_name()) - else: - logger.info("Detected %s", mount.get_name()) - 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 - 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 - path = self.prefs.device_location - if path: - logger.info("Using manually specified path %s", path) - if utilities.is_directory(path): - mounts.append((path, None)) - else: - logger.error("Download path does not exist: %s", path) - - if self.prefs.backup_images: - 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_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 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. - Depending on the folder the user has chosen, the paths for photo and - video backup will either be the same or they will differ. - """ - # user manually specified backup locations - # will backup to these paths, but don't need any volume info - # associated with them - self.backup_devices[self.prefs.backup_location] = (None, PHOTO_BACKUP) - if DOWNLOAD_VIDEO: - if self.prefs.backup_location <> self.prefs.backup_video_location: - self.backup_devices[self.prefs.backup_video_location] = (None, VIDEO_BACKUP) - logger.info("Backing up photos to %s", self.prefs.backup_location) - logger.info("Backing up videos to %s", self.prefs.backup_video_location) - else: - # videos and photos are being backed up to the same location - self.backup_devices[self.prefs.backup_location] = (None, PHOTO_VIDEO_BACKUP) - logger.info("Backing up photos and videos to %s", self.prefs.backup_location) - else: - logger.info("Backing up photos to %s", self.prefs.backup_location) - - def _add_backup_devices(self): - """ - Add each backup devices / path to backup manager - """ - for path in self.backup_devices: - name = self._backup_device_name(path) - backup_type = self.backup_devices[path][1] - self.backup_manager.add_device(path, name, backup_type) - - - def get_use_device(self, device): - """ Prompt user whether or not to download from this device """ - - 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, - # or the actual preferences will not be updated!! - if len(self.prefs.device_whitelist): - self.prefs.device_whitelist = self.prefs.device_whitelist + [path] - else: - 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] - - def search_for_PSD(self): - """ - 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 - 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) - """ - if self.prefs.backup_images: - if self.prefs.backup_device_autodetection: - # Determine if the auto-detected backup device is - # to be used to backup only photos, or videos, or both. - # Use the presence of a corresponding directory to - # determine this. - # The directory must be writable. - photo_path = os.path.join(path, self.prefs.backup_identifier) - p_backup = os.path.isdir(photo_path) and os.access(photo_path, os.W_OK) - if DOWNLOAD_VIDEO: - video_path = os.path.join(path, self.prefs.video_backup_identifier) - v_backup = os.path.isdir(video_path) and os.access(video_path, os.W_OK) - else: - v_backup = False - if p_backup and v_backup: - logger.info("Photos and videos will be backed up to %s", path) - return (True, PHOTO_VIDEO_BACKUP) - elif p_backup: - logger.info("Photos will be backed up to %s", path) - return (True, PHOTO_BACKUP) - elif v_backup: - logger.info("Videos will be backed up to %s", path) - return (True, VIDEO_BACKUP) - elif path == self.prefs.backup_location: - # user manually specified the path - if os.access(self.prefs.backup_location, os.W_OK): - return (True, PHOTO_BACKUP) - elif path == self.prefs.backup_video_location: - # user manually specified the path - if os.access(self.prefs.backup_video_location, os.W_OK): - return (True, VIDEO_BACKUP) - return (False, None) - - def update_no_backup_devices(self): - self.no_photo_backup_devices = 0 - self.no_video_backup_devices = 0 - for path, value in self.backup_devices.iteritems(): - backup_type = value[1] - if backup_type == PHOTO_BACKUP: - self.no_photo_backup_devices += 1 - elif backup_type == VIDEO_BACKUP: - self.no_video_backup_devices += 1 - else: - #both videos and photos are backed up to this device / path - self.no_photo_backup_devices += 1 - self.no_video_backup_devices += 1 - logger.info("# photo backup devices: %s; # video backup devices: %s", - self.no_photo_backup_devices, self.no_video_backup_devices) - self.download_tracker.set_no_backup_devices(self.no_photo_backup_devices, - self.no_video_backup_devices) - - def refresh_backup_media(self): - """ - Setup the backup media - - 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: - self._setup_manual_backup() - else: - for mount in self.vmonitor.get_mounts(): - if not mount.is_shadowed(): - path = mount.get_root().get_path() - if path: - is_backup_mount, backup_file_type = self.check_if_backup_mount(path) - if is_backup_mount: - # is a backup volume - if path not in self.backup_devices: - self.backup_devices[path] = (mount, 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 - self.prefs.backup_device_autodetection - )) - - def on_mount_added(self, vmonitor, mount): - """ - callback run when gio indicates a new volume - has been mounted - """ - - - if mount.is_shadowed(): - # ignore this type of mount - return - - path = mount.get_root().get_path() - if path is not None: - - if path in self.prefs.device_blacklist and self.search_for_PSD(): - logger.info("Device %(device)s (%(path)s) ignored" % { - '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 - name = self._backup_device_name(path) - self.backup_manager.add_device(path, name, backup_file_type) - self.update_no_backup_devices() - self.display_free_space() - - 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 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, - 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() - else: - self.start_download() - 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(): - sensitivity = False - 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 - """ - - 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() - self.prompting_for_job_code = False - - 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, - 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() - - 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 - """ - # Track download sizes and other values for each device. - # (Scan id acts as an index to each device. A device could be scanned - # more than once). - self.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 or verify files during download""" - return self.prefs.auto_rotate_jpeg or (self.focal_length is not None) or self.prefs.verify_file - - - 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) - self.check_file_types_to_be_downloaded(files_by_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") % { - 'folder1': invalid_dirs[0], 'folder2': invalid_dirs[1]} - else: - msg = _("This download folder is invalid:\n%s") % invalid_dirs[0] - self.log_error(config.CRITICAL_ERROR, _("Download cannot proceed"), - msg) - else: - missing_destinations = self.backup_destinations_missing() - if missing_destinations is not None: - # Warn user that they have specified that they want to backup a file type, but no such folder exists on backup devices - if not missing_destinations[0]: - logger.warning("No backup device contains a valid folder for backing up photos") - msg = _("No backup device contains a valid folder for backing up %(filetype)s") % {'filetype': _('photos')} - else: - logger.warning("No backup device contains a valid folder for backing up videos") - msg = _("No backup device contains a valid folder for backing up %(filetype)s") % {'filetype': _('videos')} - - self.log_error(config.WARNING, _("Backup problem"), msg) - - # 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() - - # 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): - """ - 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, - 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, - rpdfile.FILE_TYPE_VIDEO, - return_file_count=True) - if no_videos_to_download: - video_download_folder = self.prefs.video_download_folder - else: - video_download_folder = None - else: - video_download_folder = None - no_videos_to_download = 0 - - 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, - 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 - - verify_file = self.prefs.verify_file - if verify_file: - # since a file might be modified in the file modify process, - # if it will be backed up, need to refresh the md5 once it has - # been modified - refresh_md5_on_file_change = self.prefs.backup_images - else: - refresh_md5_on_file_change = False - modify_files_during_download = self.modify_files_during_download() - if modify_files_during_download: - self.file_modify_manager.add_task((scan_pid, self.prefs.auto_rotate_jpeg, self.focal_length, verify_file, refresh_md5_on_file_change)) - modify_pipe = self.file_modify_manager.get_modify_pipe(scan_pid) - else: - modify_pipe = None - - - # Initiate copy files process - self.copy_files_manager.add_task((photo_download_folder, - video_download_folder, scan_pid, - files, verify_file, - modify_files_during_download, - modify_pipe)) - - def copy_files_results(self, source, condition): - """ - Handle results from copy files process - """ - #FIXME: must handle early termination / pause of copy files process - connection = self.copy_files_manager.get_pipe(source) - conn_type, msg_data = connection.recv() - if conn_type == rpdmp.CONN_PARTIAL: - msg_type, data = msg_data - - 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: - logger.debug("Using temp dirs %s (photos) & %s (videos)", - photo_temp_dir, video_temp_dir) - elif photo_temp_dir: - logger.debug("Using temp dir %s (photos)", - photo_temp_dir) - else: - logger.debug("Using temp dir %s (videos)", - 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, - total_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: - 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, - thumbnail)) - - self.download_tracker.set_download_count_for_file( - rpd_file.unique_id, download_count) - self.download_tracker.set_download_count( - rpd_file.scan_pid, download_count) - rpd_file.download_start_time = self.download_start_time - - if download_succeeded: - # Insert preference values needed for name generation - rpd_file = prefsrapid.insert_pref_lists(self.prefs, rpd_file) - rpd_file.strip_characters = self.prefs.strip_characters - rpd_file.download_folder = self.prefs.get_download_folder_for_file_type(rpd_file.file_type) - rpd_file.download_conflict_resolution = self.prefs.download_conflict_resolution - rpd_file.synchronize_raw_jpg = self.prefs.must_synchronize_raw_jpg() - rpd_file.job_code = self.job_code - - # Call this even if download did not succeed e.g. file verification error - self.subfolder_file_manager.rename_file_and_move_to_subfolder( - download_succeeded, - download_count, - rpd_file - ) - - def file_modify_results(self, source, condition): - """ - 'file modify' is a process that runs immediately after 'copy files', - meaning there can be more than one at one time. - - It runs before the renaming process. - """ - connection = self.file_modify_manager.get_pipe(source) - - conn_type, data = connection.recv() - if conn_type == rpdmp.CONN_PARTIAL: - self.copy_file_results_single_file(data) - return True - else: - # Process is complete, i.e. conn_type == rpdmp.CONN_COMPLETE - connection.close() - return False - - - def download_is_occurring(self): - """Returns True if a file is currently being downloaded, renamed or - 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, 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, - rpd_file.error_msg, rpd_file.error_extra_detail) - - if self.prefs.backup_images: - if self.backup_possible(rpd_file.file_type): - if self.prefs.backup_device_autodetection: - if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: - path_suffix = self.prefs.backup_identifier - else: - path_suffix = self.prefs.video_backup_identifier - else: - path_suffix = None - - self.backup_manager.backup_file(move_succeeded, rpd_file, - path_suffix, - self.prefs.backup_duplicate_overwrite, - self.prefs.verify_file, - download_count) - else: - if rpd_file.status == config.STATUS_DOWNLOAD_FAILED: - rpd_file.status = config.STATUS_DOWNLOAD_AND_BACKUP_FAILED - else: - rpd_file.status = config.STATUS_BACKUP_PROBLEM - - self.file_download_finished(move_succeeded, rpd_file) - else: - self.file_download_finished(move_succeeded, rpd_file) - - def backup_possible(self, file_type): - if file_type == rpdfile.FILE_TYPE_PHOTO: - return self.no_photo_backup_devices > 0 - elif file_type == rpdfile.FILE_TYPE_VIDEO: - return self.no_video_backup_devices > 0 - else: - logger.critical("Unrecognized file type when determining if backup is possible") - - - def multiple_backup_devices(self, file_type): - """Returns true if more than one backup device is being used for that - file type - """ - return ((file_type == rpdfile.FILE_TYPE_PHOTO and - self.no_photo_backup_devices > 1) or - (file_type == rpdfile.FILE_TYPE_VIDEO and - self.no_video_backup_devices > 1)) - - def backup_results(self, source, condition): - """ - Handle results sent from backup processes - """ - connection = self.backup_manager.get_pipe(source) - conn_type, msg_data = connection.recv() - if conn_type == rpdmp.CONN_PARTIAL: - 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, - 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, do_backup, 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 rely 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) and do_backup: - self.log_error(config.SERIOUS_ERROR, - rpd_file.error_title, - rpd_file.error_msg, rpd_file.error_extra_detail) - - if do_backup: - 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): - logger.debug("File %s will not be backed up to any more locations", rpd_file.download_name) - self.file_download_finished(backup_succeeded or not do_backup, rpd_file) - return True - 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 - """ - scan_pid = rpd_file.scan_pid - 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, - rpd_file.error_msg, rpd_file.error_extra_detail) - elif self.prefs.auto_delete: - # 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, - 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") - self._clean_temp_dirs_for_scan_pid(scan_pid) - if self.prefs.auto_delete: - logger.debug("Auto deleting files") - self.auto_delete(scan_pid) - self.download_tracker.clear_auto_delete(scan_pid) - self.download_active_by_scan_pid.remove(scan_pid) - self.time_remaining.remove(scan_pid) - 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()) - 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 - elif secs == 60: - message = _("About 1 minute remaining") - else: - # 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) - - 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): - f = gio.File(file) - try: - f.delete(cancellable=None) - except gio.Error, inst: - logger.error("Failure deleting file %s", file) - 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 - """ - if (no_videos > 0) and (no_photos > 0): - v = _('photos and videos') - elif (no_videos == 0) and (no_photos == 0): - v = _('photos or videos') - elif no_videos > 0: - if no_videos > 1: - v = _('videos') - else: - v = _('video') - else: - if no_photos > 1: - v = _('photos') - else: - v = _('photo') - return v - - 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( - scan_pid, rpdfile.FILE_TYPE_VIDEO) - no_photos_failed = self.download_tracker.get_no_files_failed( - scan_pid, rpdfile.FILE_TYPE_PHOTO) - no_videos_failed = self.download_tracker.get_no_files_failed( - scan_pid, rpdfile.FILE_TYPE_VIDEO) - 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") - - if use_pynotify: - n = pynotify.Notification(notification_name, message) - n.set_icon_from_pixbuf(icon) - - try: - n.show() - except: - logger.error("Unable to display message using notification system") - - 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, - 'numberdownloaded': _("%(filetype)s downloaded") % \ - {'filetype': filetype}} - - # photo failures - photo_failures = self.download_tracker.total_photo_failures - if photo_failures: - filetype = self.file_types_by_number(photo_failures, 0) - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'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, - 'numberdownloaded': _("%(filetype)s downloaded") % \ - {'filetype': filetype}} - - # video failures - video_failures = self.download_tracker.total_video_failures - if video_failures: - filetype = self.file_types_by_number(0, video_failures) - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': video_failures, - 'numberdownloaded': _("%(filetype)s failed to download") % \ - {'filetype': filetype}} - - # warnings - warnings = self.download_tracker.total_warnings - if warnings: - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': warnings, - 'numberdownloaded': _("warnings")} - - if use_pynotify: - n = pynotify.Notification(PROGRAM_NAME, message) - n.set_icon_from_pixbuf(self.application_icon) - try: - n.show() - except: - logger.error("Unable to display message using notification system") - 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 self.backup_possible(file_type)): - 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, - '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, - '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, - 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): - """ - Cleans all temp dirs if they exist - """ - 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 - """ - for temp_dir in self.temp_dirs_by_scan_pid[scan_pid]: - self._purge_dir(temp_dir) - del self.temp_dirs_by_scan_pid[scan_pid] - - 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) - # first delete any files in the temp directory - # assume there are no directories in the temp directory - file_attributes = "standard::name,standard::type" - children = path.enumerate_children(file_attributes) - for child in children: - f = path.get_child(child.get_name()) - f.delete(cancellable=None) - path.delete(cancellable=None) - logger.debug("Deleted directory %s", directory) - except gio.Error, inst: - logger.error("Failure deleting temporary folder %s", directory) - logger.error(inst) - - - # # # - # Preferences - # # # - - - 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: - self.prefs.device_autodetection = True - 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 - # 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 - # 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 / - # 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") - - 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.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() - - def check_prefs_upgrade(self, running_version): - """ - Checks if the running version of the program is different from the - version recorded in the preferences. - - If the version is different, the preferences are checked to see - whether they should be upgraded or not. - """ - previous_version = self.prefs.program_version - if len(previous_version) > 0: - # the program has been run previously for this user - - pv = utilities.pythonify_version(previous_version) - rv = utilities.pythonify_version(running_version) - - if pv <> rv: - # 0.4.1 and below had only one manual backup location - # 0.4.2 introduced a distinct video back up location that can be manually set - # Therefore must duplicate the previous photo & video manual backup location into the - # new video field, unless it has already been changed already. - - if pv < utilities.pythonify_version('0.4.2'): - if self.prefs.backup_video_location == os.path.expanduser('~'): - self.prefs.backup_video_location = self.prefs.backup_location - logger.info("Migrated manual backup location preference to videos: %s", - self.prefs.backup_video_location) - - def on_preference_changed(self, key, value): - """ - Called when user changes the program's preferences - """ - logger.debug("Preference change detected: %s", key) - - if key == 'show_log_dialog': - self.menu_log_window.set_active(value) - 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', - '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() - 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.") - - do_not_size_icon = False - self.notification_icon_size = 48 - try: - info = pynotify.get_server_info() - except: - logger.warning("Desktop environment notification server is incorrectly configured.") - else: - try: - if info["name"] == 'notify-osd': - 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')) - else: - self.application_icon = gtk.gdk.pixbuf_new_from_file_at_size( - paths.share_dir('glade3/rapid-photo-downloader.svg'), - self.notification_icon_size, self.notification_icon_size) - - def _init_widgets(self): - """ - Initialize widgets in the main window, and variables that point to them - """ - builder = gtk.Builder() - builder.set_translation_domain(config.APP_NAME) - self.builder = builder - builder.add_from_file(paths.share_dir("glade3/rapid.ui")) - self.rapidapp = builder.get_object("rapidapp") - self.from_toolbar = builder.get_object("from_toolbar") - self.copy_toolbar = builder.get_object("copy_toolbar") - self.dest_toolbar = builder.get_object("dest_toolbar") - self.menu_toolbar = builder.get_object("menu_toolbar") - self.main_vpaned = builder.get_object("main_vpaned") - self.main_notebook = builder.get_object("main_notebook") - self.download_action = builder.get_object("download_action") - self.download_button = builder.get_object("download_button") - - self.download_progressbar = builder.get_object("download_progressbar") - self.rapid_statusbar = builder.get_object("rapid_statusbar") - self.statusbar_context_id = self.rapid_statusbar.get_context_id("progress") - self.device_collection_scrolledwindow = builder.get_object("device_collection_scrolledwindow") - self.next_image_action = builder.get_object("next_image_action") - self.prev_image_action = builder.get_object("prev_image_action") - self.menu_log_window = builder.get_object("menu_log_window") - 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) - - #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() - self.from_toolbar_label.add(from_label) - self.from_toolbar_label.set_is_important(True) - self.from_toolbar.insert(self.from_toolbar_label, 0) - - self.auto_detect_button = gtk.ToggleToolButton() - self.auto_detect_button.set_is_important(True) - self.auto_detect_button.set_label(_("Auto Detect")) - self.from_toolbar.insert(self.auto_detect_button, 1) - - self.from_filechooser_button = gtk.FileChooserButton( - _("Select a folder containing %(file_types)s") % {'file_types':file_types_to_download()}) - self.from_filechooser_button.set_action( - gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) - - self.from_filechooser = gtk.ToolItem() - self.from_filechooser.set_is_important(True) - self.from_filechooser.add(self.from_filechooser_button) - self.from_filechooser.set_expand(True) - self.from_toolbar.insert(self.from_filechooser, 2) - - self._set_from_toolbar_state() - - #set events after having initialized the values - self.auto_detect_button.connect("toggled", self.on_auto_detect_button_toggled_event) - self.from_filechooser_button.connect("selection-changed", - self.on_from_filechooser_button_selection_changed) - - self.from_toolbar.show_all() - - def _setup_copy_move_toolbar(self): - self.copy_toolbar.set_style(gtk.TOOLBAR_TEXT) - self.copy_toolbar.set_border_width(5) - - copy_move_label = gtk.Label(" ") - self.copy_move_toolbar_label = gtk.ToolItem() - self.copy_move_toolbar_label.add(copy_move_label) - self.copy_move_toolbar_label.set_is_important(True) - self.copy_toolbar.insert(self.copy_move_toolbar_label, 0) - - self.copy_hbox = gtk.HBox() - self.move_hbox = gtk.HBox() - self.forward_image = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_SMALL_TOOLBAR) - self.forward_image2 = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_SMALL_TOOLBAR) - self.forward_image3 = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_SMALL_TOOLBAR) - self.forward_image4 = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_SMALL_TOOLBAR) - self.forward_label = gtk.Label(" ") - self.forward_label2 = gtk.Label(" ") - self.forward_label3 = gtk.Label(" ") - self.forward_label4 = gtk.Label(" ") - - self.copy_button = gtk.RadioToolButton() - self.copy_button.set_label(_("Copy")) - self.copy_button.set_is_important(True) - - self.copy_hbox.pack_start(self.forward_label) - self.copy_hbox.pack_start(self.forward_image) - self.copy_hbox.pack_start(self.copy_button, expand=False, fill=False) - self.copy_hbox.pack_start(self.forward_image2) - self.copy_hbox.pack_start(self.forward_label2) - copy_box = gtk.ToolItem() - copy_box.add(self.copy_hbox) - self.copy_toolbar.insert(copy_box, 1) - - self.move_button = gtk.RadioToolButton(self.copy_button) - self.move_button.set_label(_("Move")) - self.move_button.set_is_important(True) - self.move_hbox.pack_start(self.forward_label3) - self.move_hbox.pack_start(self.forward_image3) - self.move_hbox.pack_start(self.move_button, expand=False, fill=False) - self.move_hbox.pack_start(self.forward_image4) - self.move_hbox.pack_start(self.forward_label4) - move_box = gtk.ToolItem() - move_box.add(self.move_hbox) - self.copy_toolbar.insert(move_box, 2) - - self.move_button.set_active(self.prefs.auto_delete) - self.copy_button.connect("toggled", self.on_copy_button_toggle_event) - - self.copy_toolbar.show_all() - self._set_copy_toolbar_active_arrows() - - def _setup_dest_toolbar(self): - #Destination Toolbar - self.dest_toolbar.set_border_width(5) - - dest_label = gtk.Label() - dest_label.set_markup("<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( - gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) - photo_dest_hbox.pack_start(self.photo_dest_label, expand=False, fill=False, padding=6) - photo_dest_hbox.pack_start(self.to_photo_filechooser_button) - self.to_photo_filechooser = gtk.ToolItem() - self.to_photo_filechooser.set_is_important(True) - self.to_photo_filechooser.set_expand(True) - self.to_photo_filechooser.add(photo_dest_hbox) - self.dest_toolbar.insert(self.to_photo_filechooser, 1) - - video_dest_hbox = gtk.HBox() - self.video_dest_label = gtk.Label(_("Videos:")) - self.to_video_filechooser_button = gtk.FileChooserButton( - _("Select a folder to download videos to")) - self.to_video_filechooser_button.set_action( - gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) - video_dest_hbox.pack_start(self.video_dest_label, expand=False, fill=False, padding=6) - video_dest_hbox.pack_start(self.to_video_filechooser_button) - self.to_video_filechooser = gtk.ToolItem() - self.to_video_filechooser.set_is_important(True) - self.to_video_filechooser.set_expand(True) - self.to_video_filechooser.add(video_dest_hbox) - self.dest_toolbar.insert(self.to_video_filechooser, 2) - - self._set_to_toolbar_values() - self.to_photo_filechooser_button.connect("selection-changed", - self.on_to_photo_filechooser_button_selection_changed) - #~ self.to_photo_filechooser_button.connect("file-set", - #~ self.on_to_photo_filechooser_button_file_set) - #~ self.to_photo_filechooser_button.connect("current-folder-changed", - #~ self.on_to_photo_filechooser_button_current_folder_changed) - self.to_video_filechooser_button.connect("selection-changed", - self.on_to_video_filechooser_button_selection_changed) - self.dest_toolbar.show_all() - - def _make_widget_widths_equal(self, widget1, widget2): - """takes two widgets and sets a width for both equal to widest one""" - - x1, y1, w1, h1 = widget1.get_allocation() - x2, y2, w2, h2 = widget2.get_allocation() - w = max(w1, w2) - h = max(h1, h2) - widget1.set_size_request(w,h) - widget2.set_size_request(w,h) - - def _set_copy_toolbar_active_arrows(self): - if self.copy_button.get_active(): - self.forward_image.set_visible(True) - self.forward_image2.set_visible(True) - self.forward_image3.set_visible(False) - self.forward_image4.set_visible(False) - self.forward_label.set_visible(False) - self.forward_label2.set_visible(False) - self.forward_label3.set_visible(True) - self.forward_label4.set_visible(True) - else: - self.forward_image.set_visible(False) - self.forward_image2.set_visible(False) - self.forward_image3.set_visible(True) - self.forward_image4.set_visible(True) - self.forward_label.set_visible(True) - self.forward_label2.set_visible(True) - self.forward_label3.set_visible(False) - self.forward_label4.set_visible(False) - - def on_copy_button_toggle_event(self, radio_button): - self._set_copy_toolbar_active_arrows() - self.prefs.auto_delete = not self.copy_button.get_active() - - def _set_from_toolbar_state(self): - logger.debug("_set_from_toolbar_state") - self.auto_detect_button.set_active(self.prefs.device_autodetection) - if self.prefs.device_autodetection: - self.from_filechooser_button.set_sensitive(False) - self.from_filechooser_button.set_current_folder(self.prefs.device_location) - - def on_auto_detect_button_toggled_event(self, button): - logger.debug("on_auto_detect_button_toggled_event") - self.from_filechooser_button.set_sensitive(not button.get_active()) - if not self.rerun_setup_available_image_and_video_media: - self.prefs.device_autodetection = button.get_active() - - def on_from_filechooser_button_selection_changed(self, filechooserbutton): - logger.debug("on_from_filechooser_button_selection_changed") - path = get_folder_selection(filechooserbutton) - if path and not self.rerun_setup_available_image_and_video_media: - self.prefs.device_location = path - - def on_from_filechooser_button_file_set(self, button): - logger.debug("on_from_filechooser_button_file_set") - - def on_to_photo_filechooser_button_file_set(self, filechooserbutton): - logger.debug("on_to_filechooser_button_file_set") - - def on_to_photo_filechooser_button_selection_changed(self, filechooserbutton): - logger.debug("on_to_filechooser_button_selection_changed") - path = get_folder_selection(filechooserbutton) - #~ logger.debug("Path: %s", path) - if path: - self.prefs.download_folder = path - - def on_to_photo_filechooser_button_current_folder_changed(self, filechooserbutton): - logger.debug("on_to_photo_filechooser_button_current_folder_changed") - - def on_to_video_filechooser_button_selection_changed(self, filechooserbutton): - path = get_folder_selection(filechooserbutton) - if path: - self.prefs.video_download_folder = path - - def _set_to_toolbar_values(self): - self.to_photo_filechooser_button.set_current_folder(self.prefs.download_folder) - self.to_video_filechooser_button.set_current_folder(self.prefs.video_download_folder) - - def toolbar_event(self, widget, toolbar): - pass - - - - def _set_window_size(self): - """ - Remember the window size from the last time the program was run, or - set a default size - """ - - if self.prefs.main_window_maximized: - self.rapidapp.maximize() - 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, - config.DEFAULT_WINDOW_HEIGHT) - - - def _set_device_collection_size(self): - """ - Set the size of the device collection scrolled window widget - """ - if self.device_collection.map_process_to_row: - height = max(self.device_collection_viewport.size_request()[1], 24) - self.device_collection_scrolledwindow.set_size_request(-1, height) - self.main_vpaned.set_position(height) - else: - # 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] - register_iconsets(icon_list) - - def _setup_error_icons(self): - """ - hide display of warning and error symbols in the taskbar until they - are needed - """ - self.error_image = self.builder.get_object("error_image") - self.warning_image = self.builder.get_object("warning_image") - self.warning_vseparator = self.builder.get_object("warning_vseparator") - 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 - or refresh command. - The intention is to be able to disable this during a download - """ - 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 - mounts will be used - """ - message = '' - - paths = self.backup_devices.keys() - i = 0 - v = len(paths) - prefix = '' - for b in paths: - if v > 1: - if i < (v -1) and i > 0: - prefix = ', ' - elif i == (v - 1) : - 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 - downloaded to. - - 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) - if photo_dir and video_dir: - same_file_system = self.same_file_system(self.prefs.download_folder, - 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]) - file_info = folder.query_filesystem_info(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE) - 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 - #user in the status bar. - #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 - 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 - #Used to differentiate between two different file systems - #e.g. Free space: 21.3GB (photos); 14.7GB (videos). - msg += _("; ") - 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). - 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 not self.prefs.backup_device_autodetection: - if self.prefs.backup_location == self.prefs.backup_video_location: - if DOWNLOAD_VIDEO: - # user manually specified the same location for photos and video backups - msg2 = _('Backing up photos and videos to %(path)s') % {'path':self.prefs.backup_location} - else: - # user manually specified backup location - msg2 = _('Backing up to %(path)s') % {'path':self.prefs.backup_location} - else: - # user manually specified different locations for photo and video backups - msg2 = _('Backing up photos to %(path)s and videos to %(path2)s') % { - 'path':self.prefs.backup_location, - 'path2': self.prefs.backup_video_location} - else: - msg2 = self.display_backup_mounts() - - 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() - - - def on_menu_log_window_toggled(self, widget): - active = widget.get_active() - self.prefs.show_log_dialog = active - if active: - self.error_log.widget.show() - else: - self.error_log.widget.hide() - - 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 - # # # - - def files_of_type_present(self, files, file_type, return_file_count=False): - """ - Returns true if there is at least one instance of the file_type - in the list of files to be copied - - If return_file_count is True, then the number of files of that type - will be counted and returned instead of True or False - """ - i = 0 - for rpd_file in files: - if rpd_file.file_type == file_type: - if return_file_count: - i += 1 - else: - return True - if not return_file_count: - return False - else: - return i - - def size_files_to_be_downloaded(self, files): - """ - Returns the total sizes of the photos and videos to be downloaded in bytes - """ - photo_size = 0 - video_size = 0 - for rpd_file in files: - if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: - photo_size += rpd_file.size - else: - video_size += rpd_file.size - - return (photo_size, video_size) - - def check_file_types_to_be_downloaded(self, files_by_scan_pid): - """Determines what types of files need to be downloaded, setting - self.downloading_photos and self.downloading_videos accordingly""" - self.downloading_photos = False - self.downloading_videos = False - while not self.downloading_photos and not self.downloading_videos: - for scan_pid in files_by_scan_pid: - files = files_by_scan_pid[scan_pid] - if not self.downloading_photos: - if self.files_of_type_present(files, rpdfile.FILE_TYPE_PHOTO): - self.downloading_photos = True - if not self.downloading_videos: - if self.files_of_type_present(files, rpdfile.FILE_TYPE_VIDEO): - self.downloading_videos = True - - - 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. - """ - valid = True - invalid_dirs = [] - - if self.downloading_photos: - 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", - self.prefs.download_folder) - - if self.downloading_videos: - if not self.is_valid_download_dir(self.prefs.video_download_folder, - is_photo_dir=False): - valid = False - invalid_dirs.append(self.prefs.video_download_folder) - else: - logger.debug("Video download folder is valid: %s", - self.prefs.video_download_folder) - - - return (valid, invalid_dirs) - - def backup_destinations_missing(self): - if self.prefs.backup_images and self.prefs.backup_device_autodetection: - photo_backup_ok = video_backup_ok = True - if self.downloading_photos and not self.backup_possible(rpdfile.FILE_TYPE_PHOTO): - photo_backup_ok = False - if self.downloading_videos and not self.backup_possible(rpdfile.FILE_TYPE_VIDEO): - video_backup_ok = False - if photo_backup_ok and video_backup_ok: - return None - else: - return (photo_backup_ok, video_backup_ok) - return None - - def same_file_system(self, file1, file2): - """Returns True if the files / diretories are on the same file system - """ - f1 = gio.File(file1) - f2 = gio.File(file2) - f1_info = f1.query_info(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) - f1_id = f1_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) - 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 - """ - valid = False - if is_photo_dir: - download_folder_type = _("Photo") - else: - download_folder_type = _("Video") - - try: - d = gio.File(path) - if not d.query_exists(cancellable=None): - logger.error("%s download folder does not exist: %s", - download_folder_type, path) - if show_error_in_log: - severity = config.WARNING - problem = _("%(file_type)s download folder does not exist") % { - 'file_type': download_folder_type} - details = _("Folder: %s") % path - self.log_error(severity, problem, details) - else: - file_attributes = "standard::type,access::can-read,access::can-write" - file_info = d.query_filesystem_info(file_attributes) - file_type = file_info.get_file_type() - - if file_type != gio.FILE_TYPE_DIRECTORY and file_type != gio.FILE_TYPE_UNKNOWN: - logger.error("%s download folder is invalid: %s", - download_folder_type, path) - if show_error_in_log: - severity = config.WARNING - problem = _("%(file_type)s download folder is invalid") % { - 'file_type': download_folder_type} - details = _("Folder: %s") % path - self.log_error(severity, problem, details) - else: - # is the directory writable? - try: - temp_dir = tempfile.mkdtemp(prefix="rpd-tmp", dir=path) - valid = True - except: - logger.error("%s is not writable", path) - if show_error_in_log: - severity = config.WARNING - problem = _("%(file_type)s download folder is not writable") % { - 'file_type': download_folder_type} - details = _("Folder: %s") % path - self.log_error(severity, problem, details) - else: - f = gio.File(temp_dir) - f.delete(cancellable=None) - - 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.uses_stored_sequence_no_value, - self.uses_session_sequece_no_value, - self.uses_sequence_letter_value) - - # daemon process to rename files and create subfolders - self.subfolder_file_manager = SubfolderFileManager( - self.subfolder_file_results, - sequence_values) - - # process to scan source devices / paths - self.scan_manager = ScanManager(self.scan_results, self.batch_size, - self.device_collection.add_device) - - #process to copy files from source to destination - self.copy_files_manager = CopyFilesManager(self.copy_files_results, - self.batch_size_MB) - - #process to back files up - self.backup_manager = BackupFilesManager(self.backup_results, - self.batch_size_MB) - - #process to enhance files after they've been copied and before they're - #renamed - self.file_modify_manager = FileModifyManager(self.file_modify_results) - - - def scan_results(self, source, condition): - """ - 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 - size, file_type_counter, scan_pid = data - size = format_size_for_user(bytes=size) - results_summary, file_types_present = file_type_counter.summarize_file_count() - self.download_tracker.set_file_types_present(scan_pid, file_types_present) - logger.info('Found %s' % results_summary) - logger.info('Files total %s' % size) - self.device_collection.update_device(scan_pid, size) - 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")) - self.thumbnails.generate_thumbnails(scan_pid) - elif self.auto_start_is_on: - if self.need_job_code_for_naming and not self.job_code: - self.get_job_code() - else: - self.start_download(scan_pid=scan_pid) - - 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: - # partial results - if len(data) > self.batch_size: - logger.critical("incoming pipe length is unexpectedly long: %s" % len(data)) - else: - size, file_type_counter, scan_pid, rpd_files = data - size = format_size_for_user(bytes=size) - 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, - 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): - if self.is_running(): - self.rapidapp.present() - 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. - # 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')) - parser.add_option("-q", "--quiet", action="store_false", dest="verbose", help=_("only output errors to the command line")) - # image file extensions are recognized RAW files plus TIFF and JPG - parser.add_option("-e", "--extensions", action="store_true", dest="extensions", help=_("list photo and video file extensions the program recognizes and exit")) - parser.add_option("--focal-length", type=int, dest="focal_length", help="If an aperture value of 0.0 is encountered, the focal length metadata will be set to the number passed, and its aperture metadata to f/8") - parser.add_option("-a", "--auto-detect", action="store_true", dest="auto_detect", help=_("automatically detect devices from which to download, overwriting existing program preferences")) - 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]=='/': - device_location = device_location[:-1] - logger.info("Device location set from command line: %s", device_location) - else: - device_location=None - - if options.extensions: - extensions = ((rpdfile.PHOTO_EXTENSIONS, _("Photos:")), (rpdfile.VIDEO_EXTENSIONS, _("Videos:"))) - for exts, file_type in extensions: - v = '' - for e in exts[:-1]: - 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: - focal_length = None - - logger.info("Rapid Photo Downloader %s", utilities.human_readable_version(config.version)) - logger.info("Using pyexiv2 %s", metadataphoto.pyexiv2_version_info()) - logger.info("Using exiv2 %s", metadataphoto.exiv2_version_info()) - if metadataexiftool.EXIFTOOL_VERSION is None: - logger.info("Exiftool not detected") - else: - logger.info("Using exiftool %s", metadataexiftool.EXIFTOOL_VERSION) - if metadatavideo.HAVE_HACHOIR: - logger.info("Using hachoir %s", metadatavideo.version_info()) - - - if focal_length: - logger.info("Focal length of %s will be used when an aperture of 0.0 is encountered", focal_length) - - bus = dbus.SessionBus () - request = bus.request_name (config.DBUS_NAME, dbus.bus.NAME_FLAG_DO_NOT_QUEUE) - if request != dbus.bus.REQUEST_NAME_REPLY_EXISTS: - app = RapidApp(bus, '/', config.DBUS_NAME, focal_length=focal_length, - auto_detect=auto_detect, device_location=device_location) - else: - # this application is already running - print "Rapid Photo Downloader is already running" - object = bus.get_object (config.DBUS_NAME, "/") - app = dbus.Interface (object, config.DBUS_NAME) - - app.start() - -if __name__ == "__main__": - start() |