#!/usr/bin/python # -*- coding: latin1 -*- ### Copyright (C) 2007, 2008, 2009 Damon Lynch ### This program is free software; you can redistribute it and/or modify ### it under the terms of the GNU General Public License as published by ### the Free Software Foundation; either version 2 of the License, or ### (at your option) any later version. ### This program is distributed in the hope that it will be useful, ### but WITHOUT ANY WARRANTY; without even the implied warranty of ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ### GNU General Public License for more details. ### You should have received a copy of the GNU General Public License ### along with this program; if not, write to the Free Software ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #needed for python 2.5, unneeded for python 2.6 from __future__ import with_statement import sys import os import shutil import time import datetime import atexit import tempfile import webbrowser import operator import dbus import dbus.bus import dbus.service from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) from threading import Thread, Lock from thread import error as thread_error from thread import get_ident import gtk.gdk as gdk import pango try: import gio using_gio = True except ImportError: import gnomevfs using_gio = False import prefs import paths import gnomeglade from optparse import OptionParser import pynotify import ValidatedEntry import idletube as tube import config import common import misc import higdefaults as hd from media import getDefaultPhotoLocation from media import CardMedia import media import metadata import renamesubfolderprefs as rn import tableplusminus as tpm __version__ = config.version try: import pygtk pygtk.require("2.0") except: pass try: import gtk import gtk.glade except: sys.exit(1) from common import Configi18n global _ _ = Configi18n._ #Translators: if neccessary, for guidance in how to translate this program, you may see http://damonlynch.net/translate.html PROGRAM_NAME = _('Rapid Photo Downloader') def today(): return datetime.date.today().strftime('%Y-%m-%d') def cmd_line(msg): if verbose: print msg exiting = False def updateDisplay(display_queue): try: if display_queue.size() != 0: call, args = display_queue.get() if not exiting: call(*args) # else do not update display else: sys.stderr.write("Empty display queue!\n") return True except tube.EOInformation: for w in workers.getStartedWorkers(): w.join() gtk.main_quit() return False class Queue(tube.Tube): def __init__(self, maxSize = config.MAX_NO_READERS): tube.Tube.__init__(self, maxSize) def setMaxSize(self, maxSize): self.maxsize = maxSize # Module wide values - # set up thesse variable in global name space, and initialize with proper # values later # this is ugly but I don't know a better way :( display_queue = Queue() media_collection_treeview = image_hbox = log_dialog = None job_code = None need_job_code = False class ThreadManager: _workers = [] def append(self, w): self._workers.append(w) def __getitem__(self, i): return self._workers[i] def __len__(self): return len(self._workers) def disableWorker(self, thread_id): """ set so a worker will not run, or if it is running, make it quit and therefore complete """ self._workers[thread_id].manuallyDisabled = True if self._workers[thread_id].hasStarted: self._workers[thread_id].quit() else: self._workers[thread_id].doNotStart = True def _isReadyToStart(self, w): """ Returns True if the worker is ready to start and has not been disabled """ return not w.hasStarted and not w.doNotStart and not w.manuallyDisabled def _isReadyToDownload(self, w): return w.scanComplete and not w.downloadStarted and not w.doNotStart and w.isAlive() and not w.manuallyDisabled def _isDownloading(self, w): return w.downloadStarted and w.isAlive() and not w.downloadComplete def _isFinished(self, w): """ Returns True if the worker has finished running It does not signify it finished a download """ return (w.hasStarted and not w.isAlive()) or w.manuallyDisabled def completedDownload(self, w): return w.completedDownload def firstWorkerReadyToStart(self): for w in self._workers: if self._isReadyToStart(w): return w return None def firstWorkerReadyToDownload(self): for w in self._workers: if self._isReadyToDownload(w): return w return None def startWorkers(self): for w in self.getReadyToStartWorkers(): w.start() def startDownloadingWorkers(self): for w in self.getReadyToDownloadWorkers(): w.startStop() def quitAllWorkers(self): global exiting exiting = True for w in self._workers: w.quit() def getWorkers(self): for w in self._workers: yield w def getNonFinishedWorkers(self): for w in self._workers: if not self._isFinished(w): yield w def getStartedWorkers(self): for w in self._workers: if w.hasStarted: yield w def getReadyToStartWorkers(self): for w in self._workers: if self._isReadyToStart(w): yield w def getReadyToDownloadWorkers(self): for w in self._workers: if self._isReadyToDownload(w): yield w def getNotDownloadingWorkers(self): for w in self._workers: if w.hasStarted and not w.downloadStarted: yield w def noReadyToStartWorkers(self): n = 0 for w in self._workers: if self._isReadyToStart(w): n += 1 return n def noReadyToDownloadWorkers(self): n = 0 for w in self._workers: if self._isReadyToDownload(w): n += 1 return n def getRunningWorkers(self): for w in self._workers: if w.hasStarted and w.isAlive(): yield w def getDownloadingWorkers(self): for w in self._workers: if self._isDownloading(w): yield w def getPausedWorkers(self): for w in self._workers: if w.hasStarted and not w.running: yield w def getPausedDownloadingWorkers(self): for w in self._workers: if w.downloadStarted and not w.running: yield w def getWaitingForJobCodeWorkers(self): for w in self._workers: if w.waitingForJobCode: yield w def getFinishedWorkers(self): for w in self._workers: if self._isFinished(w): yield w def noDownloadingWorkers(self): i = 0 for w in self._workers: if self._isDownloading(w): i += 1 return i def noRunningWorkers(self): i = 0 for w in self._workers: if w.hasStarted and w.isAlive(): i += 1 return i def getNextThread_id(self): return len(self._workers) def printWorkerStatus(self, worker=None): if worker: l = [worker] else: l = range(len(self._workers)) for i in l: print "\nThread %i\n=======\n" % i w = self._workers[i] print "Volume / source:", w.cardMedia.prettyName(limit=0) print "Do not start:", w.doNotStart print "Started:", w.hasStarted print "Running:", w.running print "Scan completed:", w.scanComplete print "Download started:", w.downloadStarted print "Download completed:", w.downloadComplete print "Finished:", self._isFinished(w) print "Alive:", w.isAlive() print "Manually disabled:", w.manuallyDisabled, "\n" workers = ThreadManager() class RapidPreferences(prefs.Preferences): defaults = { "program_version": prefs.Value(prefs.STRING, ""), "download_folder": prefs.Value(prefs.STRING, getDefaultPhotoLocation()), "subfolder": prefs.ListValue(prefs.STRING_LIST, rn.DEFAULT_SUBFOLDER_PREFS), "image_rename": prefs.ListValue(prefs.STRING_LIST, [rn.FILENAME, rn.NAME_EXTENSION, rn.ORIGINAL_CASE]), "device_autodetection": prefs.Value(prefs.BOOL, True), "device_location": prefs.Value(prefs.STRING, os.path.expanduser('~')), "device_autodetection_psd": prefs.Value(prefs.BOOL, False), "device_whitelist": prefs.ListValue(prefs.STRING_LIST, ['']), "device_blacklist": prefs.ListValue(prefs.STRING_LIST, ['']), "backup_images": prefs.Value(prefs.BOOL, False), "backup_device_autodetection": prefs.Value(prefs.BOOL, True), "backup_identifier": prefs.Value(prefs.STRING, config.DEFAULT_BACKUP_LOCATION), "backup_location": prefs.Value(prefs.STRING, os.path.expanduser('~')), "strip_characters": prefs.Value(prefs.BOOL, True), "auto_download_at_startup": prefs.Value(prefs.BOOL, False), "auto_download_upon_device_insertion": prefs.Value(prefs.BOOL, False), "auto_unmount": prefs.Value(prefs.BOOL, False), "auto_exit": prefs.Value(prefs.BOOL, False), "indicate_download_error": prefs.Value(prefs.BOOL, True), "download_conflict_resolution": prefs.Value(prefs.STRING, config.SKIP_DOWNLOAD), "backup_duplicate_overwrite": prefs.Value(prefs.BOOL, False), "backup_missing": prefs.Value(prefs.STRING, config.IGNORE), "display_thumbnails": prefs.Value(prefs.BOOL, True), "show_log_dialog": prefs.Value(prefs.BOOL, False), "day_start": prefs.Value(prefs.STRING, "03:00"), "downloads_today": prefs.ListValue(prefs.STRING_LIST, [today(), '0']), "stored_sequence_no": prefs.Value(prefs.INT, 0), "job_codes": prefs.ListValue(prefs.STRING_LIST, [_('New York'), _('Manila'), _('Prague'), _('Helsinki'), _('Wellington'), _('Tehran'), _('Kampala'), _('Paris'), _('Berlin'), _('Sydney'), _('Budapest'), _('Rome'), _('Moscow'), _('Delhi'), _('Warsaw'), _('Jakarta'), _('Madrid'), _('Stockholm')]) } def __init__(self): prefs.Preferences.__init__(self, config.GCONF_KEY, self.defaults) def getAndMaybeResetDownloadsToday(self): v = self.getDownloadsToday() if v <= 0: self.resetDownloadsToday() return v def getDownloadsToday(self): """Returns the preference value for the number of downloads performed today If value is less than zero, that means the date has changed""" hour, minute = self.getDayStart() adjustedToday = datetime.datetime.strptime("%s %s:%s" % (self.downloads_today[0], hour, minute), "%Y-%m-%d %H:%M") now = datetime.datetime.today() if now < adjustedToday : try: return int(self.downloads_today[1]) except ValueError: sys.stderr.write(_("Invalid Downloads Today value.\n")) sys.stderr.write(_("Resetting value to zero.\n")) self.setDownloadsToday(self.downloads_today[0] , 0) return 0 else: return -1 def setDownloadsToday(self, date, value=0): self.downloads_today = [date, str(value)] def incrementDownloadsToday(self): """ returns true if day changed """ v = self.getDownloadsToday() if v >= 0: self.setDownloadsToday(self.downloads_today[0] , v + 1) return False else: self.resetDownloadsToday(1) return True def resetDownloadsToday(self, value=0): now = datetime.datetime.today() hour, minute = self.getDayStart() t = datetime.time(hour, minute) if now.time() < t: date = today() else: d = datetime.datetime.today() + datetime.timedelta(days=1) date = d.strftime(('%Y-%m-%d')) self.setDownloadsToday(date, value) def setDayStart(self, hour, minute): self.day_start = "%s:%s" % (hour, minute) def getDayStart(self): try: t1, t2 = self.day_start.split(":") return (int(t1), int(t2)) except ValueError: sys.stderr.write(_("'Start of day' preference value is corrupted.\n")) sys.stderr.write(_("Resetting to midnight.\n")) self.day_start = "0:0" return 0, 0 def getSampleJobCode(self): if self.job_codes: return self.job_codes[0] else: return '' class ImageRenameTable(tpm.TablePlusMinus): def __init__(self, parentApp, adjustScrollWindow): tpm.TablePlusMinus.__init__(self, 1, 3) self.parentApp = parentApp self.adjustScrollWindow = adjustScrollWindow if adjustScrollWindow: self.connect("size-request", self.size_adjustment) self.connect("add", self.size_adjustment) self.tableWidth = self.allocation.width # vbar is '1' if there is not vertical scroll bar # if there is a vertical scroll bar, then it will have a the width of the bar self.vbar = self.adjustScrollWindow.get_vscrollbar().allocation.width self.getParentAppPrefs() self.getPrefsFactory() if not hasattr(self, "errorTitle"): self.errorTitle = _("Error in Image Rename preferences") try: self.prefsFactory.checkPrefsForValidity() except (rn.PrefValueInvalidError, rn.PrefLengthError, rn.PrefValueKeyComboError, rn.PrefKeyError), e: sys.stderr.write(self.errorTitle + "\n") sys.stderr.write(_("Sorry,these preferences contain an error:\n")) sys.stderr.write(self.prefsFactory.formatPreferencesForPrettyPrint() + "\n") # the preferences were invalid # reset them to their default self.prefList = self.prefsFactory.defaultPrefs self.getPrefsFactory() self.updateParentAppPrefs() msg = "%s.\n" % e msg += _("Resetting to default values.") sys.stderr.write(msg) misc.run_dialog(self.errorTitle, msg, parentApp, gtk.MESSAGE_ERROR) for row in self.prefsFactory.getWidgetsBasedOnPreferences(): self.append(row) def updatePreferences(self): prefList = [] for row in self.pm_rows: for col in range(self.pm_noColumns): widget = row[col] if widget: name = widget.get_name() if name == 'GtkComboBox': value = widget.get_active_text() elif name == 'GtkEntry': value = widget.get_text() else: sys.stderr.write("Program error: Unknown preference widget!") value = '' else: value = '' prefList.append(value) self.prefList = prefList self.updateParentAppPrefs() self.prefsFactory.prefList = prefList self.updateExample() def size_adjustment(self, arg1, arg2): """ Adjust scrolledwindow width in preferences dialog to reflect width of image rename table The algorithm is complicated by the need to tak into account the presence of a vertical scrollbar""" if self.adjustScrollWindow: if self.adjustScrollWindow.get_vscrollbar().allocation.width > 1: extra = self.adjustScrollWindow.get_vscrollbar().allocation.width + 10 else: extra = 0 if self.vbar <= 1: if self.allocation.width > self.tableWidth: self.adjustScrollWindow.set_size_request(self.allocation.width + extra, -1) self.tableWidth = self.allocation.width + extra elif self.allocation.width - extra > self.tableWidth: self.adjustScrollWindow.set_size_request(self.allocation.width + extra, -1) self.tableWidth = self.allocation.width + extra self.vbar = self.adjustScrollWindow.get_vscrollbar().allocation.width def getParentAppPrefs(self): self.prefList = self.parentApp.prefs.image_rename def getPrefsFactory(self): self.prefsFactory = rn.ImageRenamePreferences(self.prefList, self, sequences = sequences) def updateParentAppPrefs(self): self.parentApp.prefs.image_rename = self.prefList def updateExampleJobCode(self): job_code = self.parentApp.prefs.getSampleJobCode() if not job_code: job_code = _('Job code') self.prefsFactory.setJobCode(job_code) def updateExample(self): self.parentApp.updateImageRenameExample() def getDefaultRow(self): return self.prefsFactory.getDefaultRow() def on_combobox_changed(self, widget, rowPosition): for col in range(self.pm_noColumns): if self.pm_rows[rowPosition][col] == widget: break selection = [] for i in range(col + 1): # ensure it is a combo box we are getting the value from w = self.pm_rows[rowPosition][i] name = w.get_name() if name == 'GtkComboBox': selection.append(w.get_active_text()) else: selection.append(w.get_text()) for i in range(col + 1, self.pm_noColumns): selection.append('') if col <> (self.pm_noColumns - 1): widgets = self.prefsFactory.getWidgetsBasedOnUserSelection(selection) for i in range(col + 1, self.pm_noColumns): oldWidget = self.pm_rows[rowPosition][i] if oldWidget: self.remove(oldWidget) if oldWidget in self.pm_callbacks: del self.pm_callbacks[oldWidget] newWidget = widgets[i] self.pm_rows[rowPosition][i] = newWidget if newWidget: self._createCallback(newWidget, rowPosition) self.attach(newWidget, i, i+1, rowPosition, rowPosition + 1) newWidget.show() self.updatePreferences() def on_entry_changed(self, widget, rowPosition): self.updatePreferences() def on_rowAdded(self, rowPosition): """ Update preferences, as a row has been added """ self.updatePreferences() # if this was the last row or 2nd to last row, and another has just been added, move vertical scrollbar down if rowPosition in range(self.pm_noRows - 3, self.pm_noRows - 2): adjustment = self.parentApp.rename_scrolledwindow.get_vadjustment() adjustment.set_value(adjustment.upper) def on_rowDeleted(self, rowPosition): """ Update preferences, as a row has been deleted """ self.updatePreferences() class SubfolderTable(ImageRenameTable): def __init__(self, parentApp, adjustScollWindow): self.errorTitle = _("Error in Download Subfolder preferences") ImageRenameTable.__init__(self, parentApp, adjustScollWindow) def getParentAppPrefs(self): self.prefList = self.parentApp.prefs.subfolder def getPrefsFactory(self): self.prefsFactory = rn.SubfolderPreferences(self.prefList, self) def updateParentAppPrefs(self): self.parentApp.prefs.subfolder = self.prefList def updateExample(self): self.parentApp.updateDownloadFolderExample() class PreferencesDialog(gnomeglade.Component): def __init__(self, parentApp): gnomeglade.Component.__init__(self, paths.share_dir(config.GLADE_FILE), "preferencesdialog") self.widget.set_transient_for(parentApp.widget) self.prefs = parentApp.prefs self._setupTabSelector() self._setupControlSpacing() # get example image data try: w = workers.firstWorkerReadyToDownload() root, self.sampleImageName = w.firstImage() image = os.path.join(root, self.sampleImageName) self.sampleImage = metadata.MetaData(image) self.sampleImage.readMetadata() except: self.sampleImage = metadata.DummyMetaData() self.sampleImageName = 'IMG_0524.CR2' # setup tabs self._setupDownloadFolderTab() self._setupImageRenameTab() self._setupRenameOptionsTab() self._setupJobCodeTab() self._setupDeviceTab() self._setupBackupTab() self._setupAutomationTab() self._setupErrorTab() self.widget.realize() #set the width of the left column for selecting values #note: this must be called after self.widget.realize(), or else the width calculation will fail width_of_widest_sel_row = self.treeview.get_background_area(1, self.treeview_column)[2] self.scrolled_window.set_size_request(width_of_widest_sel_row + 2, -1) #set the minimum width of the scolled window holding the image rename table if self.rename_scrolledwindow.get_vscrollbar(): extra = self.rename_scrolledwindow.get_vscrollbar().allocation.width + 10 else: extra = 10 self.rename_scrolledwindow.set_size_request(self.rename_table.allocation.width + extra, -1) self.widget.show() def on_preferencesdialog_destroy(self, widget): """ Delete variables from memory that cause a file descriptor to be created on a mounted media""" del self.sampleImage, self.rename_table.prefsFactory, self.subfolder_table.prefsFactory def _setupTabSelector(self): self.notebook.set_show_tabs(0) self.model = gtk.ListStore(type("")) column = gtk.TreeViewColumn() rentext = gtk.CellRendererText() column.pack_start(rentext, expand=0) column.set_attributes(rentext, text=0) self.treeview_column = column self.treeview.append_column(column) self.treeview.props.model = self.model for c in self.notebook.get_children(): label = self.notebook.get_tab_label(c).get_text() if not label.startswith("_"): self.model.append( (label,) ) # select the first value in the list store self.treeview.set_cursor(0,column) def on_download_folder_filechooser_button_selection_changed(self, widget): self.prefs.download_folder = widget.get_current_folder() self.updateDownloadFolderExample() def on_backup_folder_filechooser_button_selection_changed(self, widget): self.prefs.backup_location = widget.get_current_folder() self.updateBackupExample() def on_device_location_filechooser_button_selection_changed(self, widget): self.prefs.device_location = widget.get_current_folder() def _setupControlSpacing(self): """ set spacing of some but not all controls not currently used """ self._setupTableSpacing(self.download_folder_table) self.download_folder_table.set_row_spacing(2, hd.VERTICAL_CONTROL_SPACE) self._setupTableSpacing(self.rename_example_table) self.devices_table.set_col_spacing(0, hd.NESTED_CONTROLS_SPACE) self._setupTableSpacing(self.backup_table) self.backup_table.set_col_spacing(1, hd.NESTED_CONTROLS_SPACE) self.backup_table.set_col_spacing(2, hd.CONTROL_LABEL_SPACE) self._setupTableSpacing(self.compatibility_table) self.compatibility_table.set_row_spacing(0, hd.VERTICAL_CONTROL_LABEL_SPACE) self._setupTableSpacing(self.error_table) self.error_table.set_row_spacing(5, hd.VERTICAL_CONTROL_SPACE / 2) def _setupTableSpacing(self, table): table.set_col_spacing(0, hd.NESTED_CONTROLS_SPACE) table.set_col_spacing(1, hd.CONTROL_LABEL_SPACE) def _setupSubfolderTable(self): self.subfolder_table = SubfolderTable(self, None) self.subfolder_vbox.pack_start(self.subfolder_table) self.subfolder_table.show_all() def _setupDownloadFolderTab(self): self.download_folder_filechooser_button = gtk.FileChooserButton( _("Select a folder to download photos to")) self.download_folder_filechooser_button.set_current_folder( self.prefs.download_folder) self.download_folder_filechooser_button.set_action( gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) self.download_folder_filechooser_button.connect("selection-changed", self.on_download_folder_filechooser_button_selection_changed) self.download_folder_table.attach( self.download_folder_filechooser_button, 2, 3, 2, 3, yoptions = gtk.SHRINK) self.download_folder_filechooser_button.show() self._setupSubfolderTable() self.updateDownloadFolderExample() def _setupImageRenameTab(self): self.rename_table = ImageRenameTable(self, self.rename_scrolledwindow) self.rename_table_vbox.pack_start(self.rename_table) self.rename_table.show_all() self.original_name_label.set_markup("%s" % self.sampleImageName) self.updateImageRenameExample() def _setupRenameOptionsTab(self): # sequence numbers self.downloads_today_entry = ValidatedEntry.ValidatedEntry(ValidatedEntry.bounded(ValidatedEntry.v_int, int, 0)) self.stored_number_entry = ValidatedEntry.ValidatedEntry(ValidatedEntry.bounded(ValidatedEntry.v_int, int, 1)) self.downloads_today_entry.connect('changed', self.on_downloads_today_entry_changed) self.stored_number_entry.connect('changed', self.on_stored_number_entry_changed) v = self.prefs.getAndMaybeResetDownloadsToday() self.downloads_today_entry.set_text(str(v)) # make the displayed value of stored sequence no 1 more than actual value # so as not to confuse the user self.stored_number_entry.set_text(str(self.prefs.stored_sequence_no+1)) self.sequence_vbox.pack_start(self.downloads_today_entry, expand=True, fill=True) self.sequence_vbox.pack_start(self.stored_number_entry, expand=False) self.downloads_today_entry.show() self.stored_number_entry.show() hour, minute = self.prefs.getDayStart() self.hour_spinbutton.set_value(float(hour)) self.minute_spinbutton.set_value(float(minute)) #compatibility self.strip_characters_checkbutton.set_active( self.prefs.strip_characters) def _setupJobCodeTab(self): self.job_code_liststore = gtk.ListStore(str) column = gtk.TreeViewColumn() rentext = gtk.CellRendererText() rentext.connect('edited', self.on_job_code_edited) rentext .set_property('editable', True) column.pack_start(rentext, expand=0) column.set_attributes(rentext, text=0) self.job_code_treeview_column = column self.job_code_treeview.append_column(column) self.job_code_treeview.props.model = self.job_code_liststore for code in self.prefs.job_codes: self.job_code_liststore.append((code, )) # set multiple selections self.job_code_treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE) self.clear_job_code_button.set_image(gtk.image_new_from_stock( gtk.STOCK_CLEAR, gtk.ICON_SIZE_BUTTON)) def _setupDeviceTab(self): self.device_location_filechooser_button = gtk.FileChooserButton( _("Select an image folder")) self.device_location_filechooser_button.set_current_folder( self.prefs.device_location) self.device_location_filechooser_button.set_action( gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) self.device_location_filechooser_button.connect("selection-changed", self.on_device_location_filechooser_button_selection_changed) self.devices2_table.attach(self.device_location_filechooser_button, 1, 2, 1, 2, xoptions = gtk.EXPAND|gtk.FILL, yoptions = gtk.SHRINK) self.device_location_filechooser_button.show() self.autodetect_device_checkbutton.set_active( self.prefs.device_autodetection) self.autodetect_psd_checkbutton.set_active( self.prefs.device_autodetection_psd) self.updateDeviceControls() def _setupBackupTab(self): self.backup_folder_filechooser_button = gtk.FileChooserButton( _("Select a folder in which to backup images")) self.backup_folder_filechooser_button.set_current_folder( self.prefs.backup_location) self.backup_folder_filechooser_button.set_action( gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) self.backup_folder_filechooser_button.connect("selection-changed", self.on_backup_folder_filechooser_button_selection_changed) self.backup_table.attach(self.backup_folder_filechooser_button, 3, 4, 7, 8, yoptions = gtk.SHRINK) self.backup_folder_filechooser_button.show() self.backup_identifier_entry.set_text(self.prefs.backup_identifier) #setup controls for manipulating sensitivity self._backupControls0 = [self.auto_detect_backup_checkbutton, self.missing_backup_label, self.backup_error_radiobutton, self.backup_warning_radiobutton, self.backup_ignore_radiobutton] self._backupControls1 = [self.backup_identifier_explanation_label, self.backup_identifier_label, self.backup_identifier_entry, self.example_backup_path_label, self.backup_example_label,] self._backupControls2 = [self.backup_location_label, self.backup_folder_filechooser_button, self.backup_location_explanation_label] self._backupControls = self._backupControls0 + self._backupControls1 + \ self._backupControls2 #assign values to checkbuttons only when other controls #have been setup, because their toggle signal is activated #when a value is assigned self.backup_checkbutton.set_active(self.prefs.backup_images) self.auto_detect_backup_checkbutton.set_active( self.prefs.backup_device_autodetection) self.updateBackupControls() self.updateBackupExample() def _setupAutomationTab(self): self.auto_startup_checkbutton.set_active( self.prefs.auto_download_at_startup) self.auto_insertion_checkbutton.set_active( self.prefs.auto_download_upon_device_insertion) self.auto_unmount_checkbutton.set_active( self.prefs.auto_unmount) self.auto_exit_checkbutton.set_active( self.prefs.auto_exit) def _setupErrorTab(self): self.indicate_download_error_checkbutton.set_active( self.prefs.indicate_download_error) if self.prefs.download_conflict_resolution == config.SKIP_DOWNLOAD: self.skip_download_radiobutton.set_active(True) else: self.add_identifier_radiobutton.set_active(True) if self.prefs.backup_missing == config.REPORT_ERROR: self.backup_error_radiobutton.set_active(True) elif self.prefs.backup_missing == config.REPORT_WARNING: self.backup_warning_radiobutton.set_active(True) else: self.backup_ignore_radiobutton.set_active(True) if self.prefs.backup_duplicate_overwrite: self.backup_duplicate_overwrite_radiobutton.set_active(True) else: self.backup_duplicate_skip_radiobutton.set_active(True) def updateImageRenameExample(self): """ Displays example image name to the user """ if hasattr(self, 'rename_table'): self.rename_table.updateExampleJobCode() name, problem = self.rename_table.prefsFactory.generateNameUsingPreferences( self.sampleImage, self.sampleImageName, self.prefs.strip_characters, sequencesPreliminary=False) else: name = problem = '' # since this is markup, escape it text = "%s" % common.escape(name) if problem: text += "\n" # Translators: please do not modify or leave out html formatting tags like and . These are used to format the text the users sees text += _("Warning: There is insufficient image metadata to fully generate the name. Please use other renaming options.") self.new_name_label.set_markup(text) def updateDownloadFolderExample(self): """ Displays example subfolder name(s) to the user """ if hasattr(self, 'subfolder_table'): self.subfolder_table.updateExampleJobCode() path, problem = self.subfolder_table.prefsFactory.generateNameUsingPreferences( self.sampleImage, self.sampleImageName, self.prefs.strip_characters) else: path = problem = '' text = os.path.join(self.prefs.download_folder, path) # since this is markup, escape it path = common.escape(text) if problem: warning = _("Warning: There is insufficient image metadata to fully generate subfolders. Please use other subfolder naming options." ) else: warning = "" # Translators: you should not modify or leave out the %s. This is a code used by the programming language python to insert a value that thes user will see self.example_download_path_label.set_markup(_("Example: %s") % text) self.subfolder_warning_label.set_markup(warning) def on_hour_spinbutton_value_changed(self, spinbutton): hour = spinbutton.get_value_as_int() minute = self.minute_spinbutton.get_value_as_int() self.prefs.setDayStart(hour, minute) self.on_downloads_today_entry_changed(self.downloads_today_entry) def on_minute_spinbutton_value_changed(self, spinbutton): hour = self.hour_spinbutton.get_value_as_int() minute = spinbutton.get_value_as_int() self.prefs.setDayStart(hour, minute) self.on_downloads_today_entry_changed(self.downloads_today_entry) def on_downloads_today_entry_changed(self, entry): if workers.noRunningWorkers() == 0: # do not update value if a download is occurring - it will mess it up! v = entry.get_text() try: v = int(v) except: v = 0 if v < 0: v = 0 self.prefs.resetDownloadsToday(v) sequences.setDownloadsToday(v) self.updateImageRenameExample() def on_stored_number_entry_changed(self, entry): if workers.noRunningWorkers() == 0: # do not update value if a download is occurring - it will mess it up! v = entry.get_text() try: # the displayed value of stored sequence no 1 more than actual value # so as not to confuse the user v = int(v) - 1 except: v = 0 if v < 0: v = 0 self.prefs.stored_sequence_no = v sequences.setStoredSequenceNo(v) self.updateImageRenameExample() def on_response(self, dialog, arg): if arg == gtk.RESPONSE_HELP: webbrowser.open("http://www.damonlynch.net/rapid/documentation") else: # arg==gtk.RESPONSE_CLOSE, or the user hit the 'x' to close the window self.prefs.backup_identifier = self.backup_identifier_entry.get_property("text") #check subfolder preferences for bad values filtered, prefList = rn.filterSubfolderPreferences(self.prefs.subfolder) if filtered: cmd_line(_("The subfolder preferences had some unnecessary values removed.")) if prefList: self.prefs.subfolder = prefList else: #Preferences list is now empty msg = _("The subfolder preferences entered are invalid and cannot be used.\nThey will be reset to their default values.") sys.stderr.write(msg + "\n") misc.run_dialog(PROGRAM_NAME, msg) self.prefs.subfolder = self.prefs.get_default("subfolder") self.widget.destroy() def on_add_job_code_button_clicked(self, button): j = JobCodeDialog(self.widget, self.prefs.job_codes, None, self.add_job_code, False) def add_job_code(self, dialog, userChoseCode, job_code, autoStart): dialog.destroy() if userChoseCode: if job_code and job_code not in self.prefs.job_codes: self.job_code_liststore.prepend((job_code, )) self.update_job_codes() selection = self.job_code_treeview.get_selection() selection.unselect_all() selection.select_path((0, )) #scroll to the top adjustment = self.job_code_scrolledwindow.get_vadjustment() adjustment.set_value(adjustment.lower) def on_remove_job_code_button_clicked(self, button): """ remove selected job codes (can be multiple selection)""" selection = self.job_code_treeview.get_selection() model, selected = selection.get_selected_rows() iters = [model.get_iter(path) for path in selected] # only delete if a jobe code is selected if iters: no = len(iters) path = None for i in range(0, no): iter = iters[i] if i == no - 1: path = model.get_path(iter) model.remove(iter) # now that we removed the selection, play nice with # the user and select the next item selection.select_path(path) # if there was no selection that meant the user # removed the last entry, so we try to select the # last item if not selection.path_is_selected(path): row = path[0]-1 # test case for empty lists if row >= 0: selection.select_path((row,)) self.update_job_codes() self.updateImageRenameExample() self.updateDownloadFolderExample() def on_clear_job_code_button_clicked(self, button): self.job_code_liststore.clear() self.update_job_codes() self.updateImageRenameExample() self.updateDownloadFolderExample() def on_job_code_edited(self, widget, path, new_text): iter = self.job_code_liststore.get_iter(path) self.job_code_liststore.set_value(iter, 0, new_text) self.update_job_codes() self.updateImageRenameExample() self.updateDownloadFolderExample() def update_job_codes(self): """ update preferences with list of job codes""" job_codes = [] for row in self.job_code_liststore: job_codes.append(row[0]) self.prefs.job_codes = job_codes def on_auto_startup_checkbutton_toggled(self, checkbutton): self.prefs.auto_download_at_startup = checkbutton.get_active() def on_auto_insertion_checkbutton_toggled(self, checkbutton): self.prefs.auto_download_upon_device_insertion = checkbutton.get_active() def on_auto_unmount_checkbutton_toggled(self, checkbutton): self.prefs.auto_unmount = checkbutton.get_active() def on_auto_exit_checkbutton_toggled(self, checkbutton): self.prefs.auto_exit = checkbutton.get_active() def on_autodetect_device_checkbutton_toggled(self, checkbutton): self.prefs.device_autodetection = checkbutton.get_active() self.updateDeviceControls() def on_autodetect_psd_checkbutton_toggled(self, checkbutton): self.prefs.device_autodetection_psd = checkbutton.get_active() def on_backup_duplicate_overwrite_radiobutton_toggled(self, widget): self.prefs.backup_duplicate_overwrite = widget.get_active() def on_backup_duplicate_skip_radiobutton_toggled(self, widget): self.prefs.backup_duplicate_overwrite = not widget.get_active() def on_backup_error_radiobutton_toggled(self, widget): self.prefs.backup_missing = config.REPORT_ERROR def on_backup_warning_radiobutton_toggled(self, widget): self.prefs.backup_missing = config.REPORT_WARNING def on_backup_ignore_radiobutton_toggled(self, widget): self.prefs.backup_missing = config.IGNORE def on_treeview_cursor_changed(self, tree): path, column = tree.get_cursor() self.notebook.set_current_page(path[0]) def on_strip_characters_checkbutton_toggled(self, check_button): self.prefs.strip_characters = check_button.get_active() self.updateImageRenameExample() self.updateDownloadFolderExample() def on_indicate_download_error_checkbutton_toggled(self, check_button): self.prefs.indicate_download_error = check_button.get_active() def on_add_identifier_radiobutton_toggled(self, widget): if widget.get_active(): self.prefs.download_conflict_resolution = config.ADD_UNIQUE_IDENTIFIER else: self.prefs.download_conflict_resolution = config.SKIP_DOWNLOAD def updateDeviceControls(self): """ Sets sensitivity of image device controls """ controls = [self.device_location_explanation_label, self.device_location_label, self.device_location_filechooser_button] if self.prefs.device_autodetection: for c in controls: c.set_sensitive(False) self.autodetect_psd_checkbutton.set_sensitive(True) self.autodetect_image_devices_label.set_sensitive(True) else: for c in controls: c.set_sensitive(True) self.autodetect_psd_checkbutton.set_sensitive(False) self.autodetect_image_devices_label.set_sensitive(False) def updateBackupControls(self): """ Sets sensitivity of backup related widgets """ if not self.backup_checkbutton.get_active(): for c in self._backupControls: c.set_sensitive(False) else: for c in self._backupControls0: c.set_sensitive(True) self.updateBackupControlsAuto() def updateBackupControlsAuto(self): """ Sets sensitivity of subset of backup related widgets """ if self.auto_detect_backup_checkbutton.get_active(): for c in self._backupControls1: c.set_sensitive(True) for c in self._backupControls2: c.set_sensitive(False) else: for c in self._backupControls1: c.set_sensitive(False) for c in self._backupControls2: c.set_sensitive(True) def on_auto_detect_backup_checkbutton_toggled(self, widget): self.prefs.backup_device_autodetection = widget.get_active() self.updateBackupControlsAuto() def on_backup_checkbutton_toggled(self, widget): self.prefs.backup_images = self.backup_checkbutton.get_active() self.updateBackupControls() def on_backup_identifier_entry_changed(self, widget): self.updateBackupExample() def on_backup_scan_folder_on_entry_changed(self, widget): self.updateBackupExample() def updateBackupExample(self): # Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. path = os.path.join(config.MEDIA_LOCATION, _("externaldrive1")) # Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. path2 = os.path.join(config.MEDIA_LOCATION, _("externaldrive2")) path = os.path.join(path, self.backup_identifier_entry.get_text()) path2 = os.path.join(path2, self.backup_identifier_entry.get_text()) path = common.escape(path) path2 = common.escape(path2) self.example_backup_path_label.set_markup("%s\n%s" % (path, path2)) class CopyPhotos(Thread): """Copies photos from source to destination, backing up if needed""" def __init__(self, thread_id, parentApp, fileRenameLock, fileSequenceLock, statsLock, downloadStats, autoStart = False, cardMedia = None): self.parentApp = parentApp self.thread_id = thread_id self.ctrl = True self.running = False self.manuallyDisabled = False # enable the capacity to block oneself with a lock # the lock will be first set when the thread begins # it will then be locked when the thread needs to be paused # releasing it will cause the code to restart from where it # left off self.lock = Lock() self.fileRenameLock = fileRenameLock self.fileSequenceLock = fileSequenceLock self.statsLock = statsLock self.downloadStats = downloadStats self.hasStarted = False self.doNotStart = False self.waitingForJobCode = False self.autoStart = autoStart self.cardMedia = cardMedia self.initializeDisplay(thread_id, self.cardMedia) self.noErrors = self.noWarnings = 0 self.scanComplete = self.downloadStarted = self.downloadComplete = False Thread.__init__(self) def initializeDisplay(self, thread_id, cardMedia = None): if self.cardMedia: media_collection_treeview.addCard(thread_id, self.cardMedia.prettyName(), '', 0, progress=0.0, # This refers to when a device like a hard drive is having its contents scanned, # looking for images. It is visible initially in the progress bar for each device # (which normally holds "x of y images copied"). # It maybe displayed only briefly if the contents of the device being scanned is small. progressBarText=_('scanning...')) def firstImage(self): """ returns name, path and size of the first image """ name, root, size, modificationTime = self.cardMedia.firstImage() return root, name def handlePreferencesError(self, e, prefsFactory): sys.stderr.write(_("Sorry,these preferences contain an error:\n")) sys.stderr.write(prefsFactory.formatPreferencesForPrettyPrint() + "\n") msg = str(e) sys.stderr.write(msg + "\n") def initializeFromPrefs(self, notifyOnError): """ Setup thread so that user preferences are handled """ self.prefs = self.parentApp.prefs self.imageRenamePrefsFactory = rn.ImageRenamePreferences(self.prefs.image_rename, self, self.fileSequenceLock, sequences) try: self.imageRenamePrefsFactory.checkPrefsForValidity() except (rn.PrefValueInvalidError, rn.PrefLengthError, rn.PrefValueKeyComboError, rn.PrefKeyError), e: if notifyOnError: self.handlePreferencesError(e, self.imageRenamePrefsFactory) raise rn.PrefError self.subfolderPrefsFactory = rn.SubfolderPreferences( self.prefs.subfolder, self) try: self.subfolderPrefsFactory.checkPrefsForValidity() except (rn.PrefValueInvalidError, rn.PrefLengthError, rn.PrefValueKeyComboError, rn.PrefKeyError), e: if notifyOnError: self.handlePreferencesError(e, self.subfolderPrefsFactory) raise rn.PrefError # copy this variable, as it is used heavily in the loop # and it is perhaps relatively expensive to read self.stripCharacters = self.prefs.strip_characters def run(self): """ Copy photos from device to local drive, and if requested, backup 1. Should the image be downloaded? 1.a generate file name 1.a.1 generate sequence numbers if needed 1.a.2 FIFO queue sequence numbers to indicate that they could potentially be used in a filename 1.b check to see if a file exists with the same name in the place it will be downloaded to 1.c if it exisits, and unique identifiers are not being used: 1.b.1 if using sequence numbers or letters, then potentially any of the sequence numbers in the queue could be used to make the filename 1.b.1.a generate and check each filename using sequence numbers in the queue 1.b.1.b if one of these filenames is unique, then image needs to be downloaded 1.b.2 do not do not download 2. Download the image 2.a copy it to temporary folder (this takes time) 2.b is the file name still unique? Perhaps a new file was created with this name in the meantime (either by another thread or another program) 2.b.1 don't allow any other thread to rename a file 2.b.2 check file name 2.b.3 adding suffix if it is not unique, being careful not to overwrite any existing file with a suffix 2.b.4 rename it to the "real"" name, effectively performing a mv 2.b.5 allow other threads to rename files 3. Backup the image, using the same filename as was used when it was downloaded 3.a does a file with the same name already exist on the backup medium? 3.b if so, user preferences determine whether it should be overwritten or not """ def getPrefs(notifyOnError): try: self.initializeFromPrefs(notifyOnError) return True except rn.PrefError: if notifyOnError: display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) msg = _("There is an error in the program preferences.") msg += _("\nPlease check preferences, restart the program, and try again.") logError(config.CRITICAL_ERROR, _("Download cannot proceed"), msg) cmd_line(_("Download cannot proceed")) cmd_line(msg) return False def scanMedia(): images = [] imageSizeSum = 0 for root, dirs, files in os.walk(self.cardMedia.getPath()): for name in files: if not self.running: self.lock.acquire() self.running = True if not self.ctrl: self.running = False display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) display_queue.close("rw") return if media.isImage(name): image = os.path.join(root, name) size = os.path.getsize(image) modificationTime = os.path.getmtime(image) images.append((name, root, size, modificationTime),) imageSizeSum += size images.sort(key=operator.itemgetter(3)) noImages = len(images) self.scanComplete = True if noImages: self.cardMedia.setMedia(images, imageSizeSum, noImages) # Translators: as already, mentioned the %s value should not be modified or left out. It may be moved if necessary. # It refers to the actual number of images that can be copied. For example, the user might see the following: # '0 of 512 images copied'. # This particular text is displayed to the user before the download has started. display = _("0 of %s images copied") % noImages display_queue.put((media_collection_treeview.updateCard, (self.thread_id, self.cardMedia.sizeOfImages(), noImages))) display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, 0.0, display, 0))) display_queue.put((self.parentApp.timeRemaining.add, (self.thread_id, imageSizeSum))) display_queue.put((self.parentApp.setDownloadButtonSensitivity, ())) # Translators: as you have already seen, the text can contain values that should not be modified or left out by you, for example %s. # This text is another example of that, but it is is a little more complex. Here there are two values which will be displayed # to the user when they run the program, signifying the number of images found, and the device they were found on. # %(number)s should be left exactly as is: 'number' should not be translated. The same applies to %(device)s: 'device' should # not be translated. Generally speaking, if translating the sentence requires it, you can move items like '%(xyz)s' around # in a sentence, but you should never modify them or leave them out. cmd_line(_("Device scan complete: found %(number)s images on %(device)s") % {'number': noImages, 'device': self.cardMedia.prettyName(limit=0)}) return True else: # it might be better to display "0 of 0" here display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) cmd_line(_("Device scan complete: no images found on %s") % self.cardMedia.prettyName(limit=0)) return False def cleanUp(): """ Cleanup functions that must be performed whether the thread exits early or when it has completed its run. """ # possibly delete any lingering files tf = os.listdir(tempWorkingDir) if tf: for f in tf: os.remove(os.path.join(tempWorkingDir, f)) os.rmdir(tempWorkingDir) def logError(severity, problem, details, resolution=None): display_queue.put((log_dialog.addMessage, (self.thread_id, severity, problem, details, resolution))) if severity == config.WARNING: self.noWarnings += 1 else: self.noErrors += 1 def checkProblemWithImageNameGeneration(newName, image, problem): if not newName: # a serious problem - a filename should never be blank! logError(config.SERIOUS_ERROR, _("Image filename could not be generated"), # '%(source)s' and '%(problem)s' are two more examples of text that should not be modified or left out _("Source: %(source)s\nProblem: %(problem)s") % {'source': image, 'problem': problem}, IMAGE_SKIPPED) elif problem: logError(config.WARNING, _("Image filename could not be properly generated. Check to ensure there is sufficient image metadata."), _("Source: %(source)s\nDestination: %(destination)s\nProblem: %(problem)s") % {'source': image, 'destination': newName, 'problem': problem}) def generateSubfolderAndFileName(image, name, needMetaDataToCreateUniqueImageName, needMetaDataToCreateUniqueSubfolderName): skipImage = False try: imageMetadata = metadata.MetaData(image) except IOError: logError(config.CRITICAL_ERROR, _("Could not open image"), _("Source: %s") % image, IMAGE_SKIPPED) skipImage = True imageMetadata = newName = newFile = path = subfolder = None else: try: # this step can fail if the source image is corrupt imageMetadata.readMetadata() except: skipImage = True if not skipImage: if not imageMetadata.exifKeys() and (needMetaDataToCreateUniqueSubfolderName or (needMetaDataToCreateUniqueImageName and not addUniqueIdentifier)): skipImage = True if skipImage: logError(config.SERIOUS_ERROR, _("Image has no metadata"), _("Metadata is essential for generating subfolders / image names.\nSource: %s") % image, IMAGE_SKIPPED) newName = newFile = path = subfolder = None else: subfolder, problem = self.subfolderPrefsFactory.generateNameUsingPreferences( imageMetadata, name, self.stripCharacters) if problem: logError(config.WARNING, _("Subfolder name could not be properly generated. Check to ensure there is sufficient image metadata."), _("Subfolder: %(subfolder)s\nImage: %(image)s\nProblem: %(problem)s") % {'subfolder': subfolder, 'image': image, 'problem': problem}) # pass the subfolder the image will go into, as this is needed to determine subfolder sequence numbers # indicate that sequences chosen should be queued newName, problem = self.imageRenamePrefsFactory.generateNameUsingPreferences( imageMetadata, name, self.stripCharacters, subfolder, sequencesPreliminary = True) path = os.path.join(baseDownloadDir, subfolder) newFile = os.path.join(path, newName) if not newName: skipImage = True checkProblemWithImageNameGeneration(newName, image, problem) return (skipImage, imageMetadata, newName, newFile, path, subfolder) def downloadImage(path, newFile, newName, originalName, image, imageMetadata, subfolder): try: imageDownloaded = False if not os.path.isdir(path): os.makedirs(path) nameUniqueBeforeCopy = True downloadNonUniqueFile = True # do a preliminary check to see if a file with the same name already exists if os.path.exists(newFile): nameUniqueBeforeCopy = False if not addUniqueIdentifier: downloadNonUniqueFile = False if usesSequenceElements: # potentially, a unique image name could still be generated # investigate this possibility with self.fileSequenceLock: for possibleName, problem in self.imageRenamePrefsFactory.generateNameSequencePossibilities(imageMetadata, originalName, self.stripCharacters, subfolder): # print "checking", possibleName, "using", originalName if possibleName: # no need to check for any problems here, it's just a temporary name possibleFile = os.path.join(path, possibleName) possibleTempFile = os.path.join(tempWorkingDir, possibleName) if not os.path.exists(possibleFile) and not os.path.exists(possibleTempFile): downloadNonUniqueFile = True break if self.prefs.indicate_download_error and not downloadNonUniqueFile: logError(config.SERIOUS_ERROR, IMAGE_ALREADY_EXISTS, _("Source: %(source)s\nDestination: %(destination)s") % {'source': image, 'destination': newFile}, IMAGE_SKIPPED) if nameUniqueBeforeCopy or downloadNonUniqueFile: tempWorkingfile = os.path.join(tempWorkingDir, newName) shutil.copy2(image, tempWorkingfile) with self.fileRenameLock: doRename = True if usesSequenceElements: with self.fileSequenceLock: # get a filename and use this as the "real" filename newName, problem = self.imageRenamePrefsFactory.generateNameUsingPreferences( imageMetadata, originalName, self.stripCharacters, subfolder, sequencesPreliminary = False) checkProblemWithImageNameGeneration(newName, image, problem) if not newName: # there was a serious error generating the filename doRename = False else: newFile = os.path.join(path, newName) # check if the file exists again if os.path.exists(newFile): if not addUniqueIdentifier: doRename = False if self.prefs.indicate_download_error: logError(config.SERIOUS_ERROR, IMAGE_ALREADY_EXISTS, _("Source: %(source)s\nDestination: %(destination)s") % {'source': image, 'destination': newFile}, IMAGE_SKIPPED) else: # add basic suffix to make the filename unique name = os.path.splitext(newName) suffixAlreadyUsed = True while suffixAlreadyUsed: if newFile in duplicate_files: duplicate_files[newFile] += 1 else: duplicate_files[newFile] = 1 identifier = '_%s' % duplicate_files[newFile] newName = name[0] + identifier + name[1] possibleNewFile = os.path.join(path, newName) suffixAlreadyUsed = os.path.exists(possibleNewFile) if self.prefs.indicate_download_error: logError(config.SERIOUS_ERROR, IMAGE_ALREADY_EXISTS, _("Source: %(source)s\nDestination: %(destination)s") % {'source': image, 'destination': newFile}, _("Unique identifier '%s' added") % identifier) newFile = possibleNewFile if doRename: os.rename(tempWorkingfile, newFile) imageDownloaded = True if usesSequenceElements: with self.fileSequenceLock: self.imageRenamePrefsFactory.sequences.imageCopySucceeded() if usesStoredSequenceNo: self.prefs.stored_sequence_no += 1 with self.fileSequenceLock: if self.prefs.incrementDownloadsToday(): # A new day, according the user's preferences of what time a day begins, has started cmd_line(_("New day has started - resetting 'Downloads Today' sequence number")) sequences.setDownloadsToday(0) except IOError, (errno, strerror): logError(config.SERIOUS_ERROR, _('Download copying error'), _("Source: %(source)s\nDestination: %(destination)s\nError: %(errorno)s %(strerror)s") % {'source': image, 'destination': newFile, 'errorno': errno, 'strerror': strerror}, _('The image was not copied.')) except OSError, (errno, strerror): logError(config.CRITICAL_ERROR, _('Download copying error'), "Source: %(source)s\nDestination: %(destination)s\nError: %(errorno)s %(strerror)s" % {'source': image, 'destination': newFile, 'errorno': errno, 'strerror': strerror}, ) if usesSequenceElements: if not imageDownloaded: self.imageRenamePrefsFactory.sequences.imageCopyFailed() return (imageDownloaded, newName, newFile) def backupImage(subfolder, newName, imageDownloaded, newFile, image): """ backup image to path(s) chosen by the user there are two scenarios: (1) image has just been downloaded and should now be backed up (2) image was already downloaded on some previous occassion and should still be backed up, because it hasn't been yet (3) image has been backed up already (or at least, a file with the same name already exists) """ try: for backupDir in self.parentApp.backupVolumes: backupPath = os.path.join(backupDir, subfolder) newBackupFile = os.path.join(backupPath, newName) copyBackup = True if os.path.exists(newBackupFile): # again, not thread safe copyBackup = self.prefs.backup_duplicate_overwrite if self.prefs.indicate_download_error: severity = config.SERIOUS_ERROR problem = _("Backup image already exists") details = _("Source: %(source)s\nDestination: %(destination)s") \ % {'source': image, 'destination': newBackupFile} if copyBackup : resolution = IMAGE_OVERWRITTEN else: resolution = IMAGE_SKIPPED logError(severity, problem, details, resolution) if copyBackup: if imageDownloaded: fileToCopy = newFile else: fileToCopy = image if not os.path.isdir(backupPath): # recreate folder structure in backup location # cannot do os.makedirs(backupPath) - it gives bad results when using external drives # we know backupDir exists # all the components of subfolder may not folders = subfolder.split(os.path.sep) folderToMake = backupDir for f in folders: if f: folderToMake = os.path.join(folderToMake, f) if not os.path.isdir(folderToMake): try: os.mkdir(folderToMake) except (errno, strerror): logError(config.SERIOUS_ERROR, _('Backing up error'), _("Destination directory could not be created\n%(directory)s\nError: %(errno)s %(strerror)s") % {'directory': folderToMake, 'errno': errno, 'strerror': strerror}, ) shutil.copy2(fileToCopy, newBackupFile) except IOError, (errno, strerror): logError(config.SERIOUS_ERROR, _('Backing up error'), _("Source: %(source)s\nDestination: %(destination)s\nError: %(errno)s %(strerror)s") % {'source': image, 'destination': newBackupFile, 'errno': errno, 'strerror': strerror}, _('The image was not copied.')) except OSError, (errno, strerror): logError(config.CRITICAL_ERROR, _('Backing up error'), _("Source: %(source)s\nDestination: %(destination)s\nError: %(errno)s %(strerror)s") % {'source': image, 'destination': newBackupFile, 'errno': errno, 'strerror': strerror} ) def notifyAndUnmount(): if not self.cardMedia.volume: unmountMessage = "" notificationName = PROGRAM_NAME else: notificationName = self.cardMedia.volume.get_name() if self.prefs.auto_unmount: self.cardMedia.volume.unmount(self.on_volume_unmount) # This message informs the user that the device (e.g. camera, hard drive or memory card) was automatically unmounted and they can now remove it unmountMessage = _("The device can now be safely removed") else: unmountMessage = "" message = _("%s images downloaded") % noImagesDownloaded if noImagesSkipped: message += "\n" + _("%s images skipped") % noImagesSkipped if unmountMessage: message = "%s\n%s" % (message, unmountMessage) if self.noWarnings: message = "%s\n%s " % (message, self.noWarnings) + _("warnings") if self.noErrors: message = "%s\n%s " % (message, self.noErrors) + _("errors") n = pynotify.Notification(notificationName, message) n.show() self.hasStarted = True display_queue.open('w') #Do not try to handle any preference errors here getPrefs(False) #check for presence of backup meditum if self.prefs.backup_images: if self.prefs.backup_missing <> config.IGNORE: if not len(self.parentApp.backupVolumes): if self.prefs.backup_missing == config.REPORT_ERROR: e = config.SERIOUS_ERROR else: e = config.WARNING logError(e, _("Backup device missing"), _("No backup device was detected.")) if not scanMedia(): cmd_line(_("This device has no images to download from.")) display_queue.close("rw") self.running = False self.lock.release() return elif self.autoStart and need_job_code: if job_code == None: self.waitingForJobCode = True display_queue.put((self.parentApp.getJobCode, ())) self.running = False self.lock.acquire() self.running = True self.waitingForJobCode = False elif not self.autoStart: # halt thread, waiting to be restarted so download proceeds self.running = False self.lock.acquire() if not self.ctrl: # thread will restart at this point, when the program is exiting # so must exit if self.ctrl indicates this self.running = False display_queue.close("rw") return self.running = True if not getPrefs(True): self.running = False display_queue.close("rw") return self.downloadStarted = True cmd_line(_("Download has started from %s") % self.cardMedia.prettyName(limit=0)) if need_job_code and job_code == None: sys.stderr.write(str(self.thread_id ) + ": job code should never be None\n") self.imageRenamePrefsFactory.setJobCode('unknown-job-code') self.subfolderPrefsFactory.setJobCode('unknown-job-code') else: self.imageRenamePrefsFactory.setJobCode(job_code) self.subfolderPrefsFactory.setJobCode(job_code) # Some images may not have metadata (this # is unlikely for images straight out of a # camera, but it is possible for images that have been edited). If # only non-dynamic components make up the rest of an image name # (e.g. text specified by the user), then relying on metadata will # likely produce duplicate names. needMetaDataToCreateUniqueImageName = self.imageRenamePrefsFactory.needImageMetaDataToCreateUniqueName() # subfolder generation also need to be examined, but here the need is # not so exacting, since subfolders contain images, and naturally the # requirement to be unique is far more relaxed. However if subfolder # generation relies entirely on metadata, that is a problem worth # looking for needMetaDataToCreateUniqueSubfolderName = self.subfolderPrefsFactory.needMetaDataToCreateUniqueName() i = 0 sizeDownloaded = noImagesDownloaded = noImagesSkipped = 0 sizeImages = self.cardMedia.sizeOfImages(humanReadable = False) display_queue.put((self.parentApp.addToTotalDownloadSize, (sizeImages, ))) display_queue.put((self.parentApp.setOverallDownloadMark, ())) display_queue.put((self.parentApp.postStartDownloadTasks, ())) sizeImages = float(sizeImages) noImages = self.cardMedia.numberOfImages() baseDownloadDir = self.prefs.download_folder #create a temporary directory in which to download the photos to #don't want to put it in system temp folder, as that is likely #to be on another partition and hence copying files from it #to the download folder will be slow! tempWorkingDir = tempfile.mkdtemp(prefix='rapid-tmp-', dir=baseDownloadDir) IMAGE_SKIPPED = _("Image skipped") IMAGE_OVERWRITTEN = _("Image overwritten") IMAGE_ALREADY_EXISTS = _("Image already exists") addUniqueIdentifier = self.prefs.download_conflict_resolution == config.ADD_UNIQUE_IDENTIFIER usesSequenceElements = self.imageRenamePrefsFactory.usesSequenceElements() usesStoredSequenceNo = self.imageRenamePrefsFactory.usesTheSequenceElement(rn.STORED_SEQ_NUMBER) sequences. setUseOfSequenceElements( self.imageRenamePrefsFactory.usesTheSequenceElement(rn.SESSION_SEQ_NUMBER), self.imageRenamePrefsFactory.usesTheSequenceElement(rn.SEQUENCE_LETTER)) while i < noImages: if not self.running: self.lock.acquire() self.running = True if not self.ctrl: self.running = False cleanUp() display_queue.close("rw") return # get information about the image to deduce image name and path name, root, size, modificationTime = self.cardMedia.images[i] image = os.path.join(root, name) skipImage, imageMetadata, newName, newFile, path, subfolder = generateSubfolderAndFileName( image, name, needMetaDataToCreateUniqueImageName, needMetaDataToCreateUniqueSubfolderName) if skipImage: noImagesSkipped += 1 else: imageDownloaded, newName, newFile = downloadImage(path, newFile, newName, name, image, imageMetadata, subfolder) if self.prefs.backup_images: backupImage(subfolder, newName, imageDownloaded, newFile, image) if imageDownloaded: noImagesDownloaded += 1 else: noImagesSkipped += 1 try: thumbnailType, thumbnail = imageMetadata.getThumbnailData() except: logError(config.WARNING, _("Image has no thumbnail"), image) thumbnail = orientation = None else: orientation = imageMetadata.orientation(missing=None) display_queue.put((image_hbox.addImage, (self.thread_id, thumbnail, orientation, image, imageDownloaded))) sizeDownloaded += size percentComplete = (sizeDownloaded / sizeImages) * 100 if sizeDownloaded == sizeImages: self.downloadComplete = True progressBarText = _("%(number)s of %(total)s images copied") % {'number': i + 1, 'total': noImages} display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, percentComplete, progressBarText, size))) i += 1 with self.statsLock: self.downloadStats.adjust(sizeDownloaded, noImagesDownloaded, noImagesSkipped, self.noWarnings, self.noErrors) # must manually delete these variables, or else the media cannot be unmounted (bug in pyexiv or exiv2) del self.subfolderPrefsFactory, self.imageRenamePrefsFactory try: del imageMetadata except: pass notifyAndUnmount() cmd_line(_("Download complete from %s") % self.cardMedia.prettyName(limit=0)) display_queue.put((self.parentApp.notifyUserAllDownloadsComplete,())) display_queue.put((self.parentApp.resetSequences,())) cleanUp() display_queue.put((self.parentApp.exitOnDownloadComplete, ())) display_queue.close("rw") self.running = False if noImages: self.lock.release() def startStop(self): if self.isAlive(): if self.running: self.running = False else: try: self.lock.release() except thread_error: sys.stderr.write(str(self.thread_id) + " thread error\n") def quit(self): """ Quits the thread A thread can be in one of four states: Not started (not alive, nothing to do) Started and actively running (alive) Started and paused (alive) Completed (not alive, nothing to do) """ if self.hasStarted: if self.isAlive(): self.ctrl = False if not self.running: released = False while not released: try: self.lock.release() released = True except thread_error: sys.stderr.write("Could not release lock for thread %s\n" % self.thread_id) def on_volume_unmount(self, data1, data2): """ needed for call to unmount volume""" pass class MediaTreeView(gtk.TreeView): """ TreeView display of memory cards and associated copying progress. Assumes a threaded environment. """ def __init__(self, parentApp): self.parentApp = parentApp # card name, size of images, number of images, copy progress, copy text self.liststore = gtk.ListStore(str, str, int, float, str) self.mapThreadToRow = {} 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) # Device refers to a thing like a camera, memory card in its reader, external hard drive, Portable Storage Device, etc. column0 = gtk.TreeViewColumn(_("Device"), gtk.CellRendererText(), text=0) 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=1) self.append_column(column1) column2 = gtk.TreeViewColumn(_("Download Progress"), gtk.CellRendererProgress(), value=3, text=4) self.append_column(column2) self.show_all() def addCard(self, thread_id, cardName, sizeImages, noImages, progress = 0.0, progressBarText = ''): if not progressBarText: progressBarText = _("0 of %s images copied") % (noImages) # add the row, and get a temporary pointer to the row iter = self.liststore.append((cardName, sizeImages, noImages, progress, progressBarText)) self._setThreadMap(thread_id, iter) # adjust scrolled window height, based on row height and number of ready to start downloads if workers.noReadyToStartWorkers() >= 1 or workers.noRunningWorkers() > 0: # please note, at program startup, self.rowHeight() 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 height = (workers.noReadyToStartWorkers() + workers.noRunningWorkers() + 2) * (self.rowHeight()) self.parentApp.media_collection_scrolledwindow.set_size_request(-1, height) def updateCard(self, thread_id, sizeImages, noImages): if thread_id in self.mapThreadToRow: iter = self._getThreadMap(thread_id) self.liststore.set_value(iter, 1, sizeImages) self.liststore.set_value(iter, 2, noImages) else: sys.stderr.write("FIXME: this card is unknown") def removeCard(self, thread_id): if thread_id in self.mapThreadToRow: iter = self._getThreadMap(thread_id) self.liststore.remove(iter) del self.mapThreadToRow[thread_id] def _setThreadMap(self, thread_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.mapThreadToRow[thread_id] = treerowRef def _getThreadMap(self, thread_id): """ return the tree iter for this thread """ treerowRef = self.mapThreadToRow[thread_id] path = treerowRef.get_path() iter = self.liststore.get_iter(path) return iter def updateProgress(self, thread_id, percentComplete, progressBarText, imageSize): iter = self._getThreadMap(thread_id) self.liststore.set_value(iter, 3, percentComplete) self.liststore.set_value(iter, 4, progressBarText) if percentComplete or imageSize: self.parentApp.updateOverallProgress(thread_id, imageSize, percentComplete) def rowHeight(self): if not self.mapThreadToRow: return 0 else: index = self.mapThreadToRow.keys()[0] path = self.mapThreadToRow[index].get_path() col = self.get_column(0) return self.get_background_area(path, col)[3] class ImageHBox(gtk.HBox): """ Displays thumbnails of the images being downloaded """ def __init__(self, parentApp): gtk.HBox.__init__(self) self.parentApp = parentApp self.padding = hd.CONTROL_IN_TABLE_SPACE / 2 #create image used to lighten thumbnails self.white = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, width=100, height=100) #fill with white self.white.fill(0xffffffff) #load missing image self.missingThumbnail = gtk.gdk.pixbuf_new_from_file_at_size(paths.share_dir('glade3/image-missing.svg'), 100, 100) def addImage(self, thread_id, thumbnail, orientation, filename, imageDownloaded): """ Add thumbnail Orientation indicates if the thumbnail needs to be rotated or not. """ if not thumbnail: pixbuf = self.missingThumbnail else: try: pbloader = gdk.PixbufLoader() pbloader.write(thumbnail) # Get the resulting pixbuf and build an image to be displayed pixbuf = pbloader.get_pixbuf() pbloader.close() except: log_dialog.addMessage(thread_id, config.WARNING, _('Thumbnail cannot be displayed'), filename, _('It may be corrupted')) pixbuf = self.missingThumbnail if not pixbuf: log_dialog.addMessage(thread_id, config.WARNING, _('Thumbnail cannot be displayed'), filename, _('It may be corrupted')) pixbuf = self.missingThumbnail else: # rotate if necessary if orientation == 8: pixbuf = pixbuf.rotate_simple(gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE) # scale to size pixbuf = common.scale2pixbuf(100, 100, pixbuf) if not imageDownloaded: # lighten it self.white.composite(pixbuf, 0, 0, pixbuf.props.width, pixbuf.props.height, 0, 0, 1.0, 1.0, gtk.gdk.INTERP_HYPER, 180) image = gtk.Image() image.set_from_pixbuf(pixbuf) self.pack_start(image, expand=False, padding=self.padding) image.show() # move viewport to display the latest image adjustment = self.parentApp.image_scrolledwindow.get_hadjustment() adjustment.set_value(adjustment.upper) class UseDeviceDialog(gtk.Dialog): def __init__(self, parent_window, path, volume, autostart, postChoiceCB): gtk.Dialog.__init__(self, _('Device Detected'), None, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, (gtk.STOCK_NO, gtk.RESPONSE_CANCEL, gtk.STOCK_YES, gtk.RESPONSE_OK)) self.postChoiceCB = postChoiceCB self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader-about.png')) # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt prompt_label = gtk.Label(_('Should this device or partition be used to download images from?')) prompt_label.set_line_wrap(True) device_label = gtk.Label() device_label.set_markup("%s" % volume.get_name(limit=0)) device_hbox = gtk.HBox() device_hbox.pack_start(device_label, False, False) path_label = gtk.Label() path_label.set_markup("%s" % path) path_hbox = gtk.HBox() path_hbox.pack_start(path_label, False, False) icon = volume.get_icon_pixbuf(36) if icon: image = gtk.Image() image.set_from_pixbuf(icon) # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt self.always_checkbutton = gtk.CheckButton(_('_Remember this choice'), True) if icon: device_hbox_icon = gtk.HBox(homogeneous=False, spacing=6) device_hbox_icon.pack_start(image, False, False, padding = 6) device_vbox = gtk.VBox(homogeneous=True, spacing=6) device_vbox.pack_start(device_hbox, False, False) device_vbox.pack_start(path_hbox, False, False) device_hbox_icon.pack_start(device_vbox, False, False) self.vbox.pack_start(device_hbox_icon, padding = 6) else: self.vbox.pack_start(device_hbox, padding=6) self.vbox.pack_start(path_hbox, padding = 6) self.vbox.pack_start(prompt_label, padding=6) self.vbox.pack_start(self.always_checkbutton, padding=6) self.set_border_width(6) self.set_has_separator(False) self.set_default_response(gtk.RESPONSE_OK) self.set_transient_for(parent_window) self.show_all() self.path = path self.volume = volume self.autostart = autostart self.connect('response', self.on_response) def on_response(self, device_dialog, response): userSelected = False permanent_choice = self.always_checkbutton.get_active() if response == gtk.RESPONSE_OK: userSelected = True # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt cmd_line(_("%s selected for downloading from" % self.volume.get_name(limit=0))) if permanent_choice: # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt cmd_line(_("This device or partition will always be used to download from")) else: # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt cmd_line(_("%s rejected as a download device" % self.volume.get_name(limit=0))) if permanent_choice: # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt cmd_line(_("This device or partition will never be used to download from")) self.postChoiceCB(self, userSelected, permanent_choice, self.path, self.volume, self.autostart) class JobCodeDialog(gtk.Dialog): """ Dialog prompting for a job code""" def __init__(self, parent_window, job_codes, default_job_code, postJobCodeEntryCB, autoStart): # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode gtk.Dialog.__init__(self, _('Enter a Job Code'), None, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OK, gtk.RESPONSE_OK)) self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader-about.png')) self.postJobCodeEntryCB = postJobCodeEntryCB self.autoStart = autoStart self.combobox = gtk.combo_box_entry_new_text() for text in job_codes: self.combobox.append_text(text) self.job_code_hbox = gtk.HBox(homogeneous = False) if len(job_codes): # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode task_label = gtk.Label(_('Enter a new job code, or select a previous one.')) else: # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode task_label = gtk.Label(_('Enter a new job code.')) task_label.set_line_wrap(True) task_hbox = gtk.HBox() task_hbox.pack_start(task_label, False, False, padding=6) label = gtk.Label(_('Job Code:')) self.job_code_hbox.pack_start(label, False, False, padding=6) self.job_code_hbox.pack_start(self.combobox, True, True, padding=6) self.set_border_width(6) self.set_has_separator(False) # make entry box have entry completion self.entry = self.combobox.child completion = gtk.EntryCompletion() completion.set_match_func(self.match_func) completion.connect("match-selected", self.on_completion_match) completion.set_model(self.combobox.get_model()) completion.set_text_column(0) self.entry.set_completion(completion) # when user hits enter, close the dialog window self.set_default_response(gtk.RESPONSE_OK) self.entry.set_activates_default(True) if default_job_code: self.entry.set_text(default_job_code) self.vbox.pack_start(task_hbox, False, False, padding = 6) self.vbox.pack_start(self.job_code_hbox, False, False, padding=12) self.set_transient_for(parent_window) self.show_all() self.connect('response', self.on_job_code_resp) def match_func(self, completion, key, iter): model = completion.get_model() return model[iter][0].startswith(self.entry.get_text()) def on_completion_match(self, completion, model, iter): self.entry.set_text(model[iter][0]) self.entry.set_position(-1) def get_job_code(self): return self.combobox.child.get_text() def on_job_code_resp(self, jc_dialog, response): userChoseCode = False if response == gtk.RESPONSE_OK: userChoseCode = True cmd_line(_("Job Code entered")) else: cmd_line(_("Job Code not entered")) self.postJobCodeEntryCB(self, userChoseCode, self.get_job_code(), self.autoStart) class LogDialog(gnomeglade.Component): """ Displays a log of errors, warnings or other information to the user """ def __init__(self, parentApp): """ Initialize values for log dialog, but do not display. """ gnomeglade.Component.__init__(self, paths.share_dir(config.GLADE_FILE), "logdialog") self.widget.connect("delete-event", self.hide_window) self.parentApp = parentApp self.log_textview.set_cursor_visible(False) self.textbuffer = self.log_textview.get_buffer() self.problemTag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD) self.resolutionTag = self.textbuffer.create_tag(style=pango.STYLE_ITALIC) def addMessage(self, thread_id, severity, problem, details, resolution): if severity in [config.CRITICAL_ERROR, config.SERIOUS_ERROR]: self.parentApp.error_image.show() elif severity == config.WARNING: self.parentApp.warning_image.show() self.parentApp.warning_vseparator.show() iter = self.textbuffer.get_end_iter() self.textbuffer.insert_with_tags(iter, problem +"\n", self.problemTag) if details: iter = self.textbuffer.get_end_iter() self.textbuffer.insert(iter, details + "\n") if resolution: iter = self.textbuffer.get_end_iter() self.textbuffer.insert_with_tags(iter, resolution +"\n", self.resolutionTag) iter = self.textbuffer.get_end_iter() self.textbuffer.insert(iter, "\n") # move viewport to display the latest message adjustment = self.log_scrolledwindow.get_vadjustment() adjustment.set_value(adjustment.upper) def on_logdialog_response(self, dialog, arg): if arg == gtk.RESPONSE_CLOSE: pass self.parentApp.error_image.hide() self.parentApp.warning_image.hide() self.parentApp.warning_vseparator.hide() self.parentApp.prefs.show_log_dialog = False self.widget.hide() return True def hide_window(self, window, event): window.hide() return True class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): def __init__(self, bus, path, name): dbus.service.Object.__init__ (self, bus, path, name) self.running = False gladefile = paths.share_dir(config.GLADE_FILE) gnomeglade.GnomeApp.__init__(self, "rapid", __version__, gladefile, "rapidapp") # notifications self.displayDownloadSummaryNotification = False self.initPyNotify() self.prefs = RapidPreferences() self.prefs.notify_add(self.on_preference_changed) self.testing = False if self.testing: self.setTestingEnv() # sys.exit(0) self.widget.show() displayPreferences = self.checkForUpgrade(__version__) self.prefs.program_version = __version__ self.timeRemaining = TimeRemaining() self._resetDownloadInfo() self.statusbar_context_id = self.rapid_statusbar.get_context_id("progress") # hide display of warning and error symbols in the taskbar until they are needed self.error_image.hide() self.warning_image.hide() self.warning_vseparator.hide() if not displayPreferences: displayPreferences = not self.checkPreferencesOnStartup() # display download information using threads global media_collection_treeview, image_hbox, log_dialog global download_queue, image_queue, log_queue global workers #track files that should have a suffix added to them global duplicate_files # control sequence numbers and letters global sequences # whether we need to prompt for a job code global need_job_code duplicate_files = {} downloadsToday = self.prefs.getAndMaybeResetDownloadsToday() sequences = rn.Sequences(downloadsToday, self.prefs.stored_sequence_no) self.downloadStats = DownloadStats() # set the number of seconds gap with which to measure download time remaing self.downloadTimeGap = 3 #locks for threadsafe file downloading and stats gathering self.fileRenameLock = Lock() self.fileSequenceLock = Lock() self.statsLock = Lock() # log window, in dialog format # used for displaying download information to the user log_dialog = LogDialog(self) self.volumeMonitor = None if self.usingVolumeMonitor(): self.startVolumeMonitor() # set up tree view display to display image devices and download status media_collection_treeview = MediaTreeView(self) self.media_collection_vbox.pack_start(media_collection_treeview) #thumbnail display image_hbox = ImageHBox(self) self.image_viewport.add(image_hbox) self.image_viewport.modify_bg(gtk.STATE_NORMAL, gdk.color_parse("white")) self.set_display_thumbnails(self.prefs.display_thumbnails) self.backupVolumes = {} self._setupDownloadbutton() #status bar progress bar self.download_progressbar = gtk.ProgressBar() self.download_progressbar.set_size_request(150, -1) self.download_progressbar.show() self.download_progressbar_hbox.pack_start(self.download_progressbar, expand=False, fill=0) # menus self.menu_display_thumbnails.set_active(self.prefs.display_thumbnails) self.menu_clear.set_sensitive(False) #job code initialization need_job_code = self.needJobCode() self.last_chosen_job_code = None self.prompting_for_job_code = False #setup download and backup mediums, initiating scans self.setupAvailableImageAndBackupMedia(onStartup=True, onPreferenceChange=False, doNotAllowAutoStart = displayPreferences) #adjust viewport size for displaying media #this is important because the code in MediaTreeView.addCard() is inaccurate at program startup height = self.media_collection_viewport.size_request()[1] self.media_collection_scrolledwindow.set_size_request(-1, height) self.download_button.grab_default() # for some reason, the grab focus command is not working... unsure why self.download_button.grab_focus() if displayPreferences: PreferencesDialog(self) @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 self.main() self.running = False def setTestingEnv(self): self.prefs.program_version = '0.0.8~b7' r = ['Date time', 'Image date', 'YYYYMMDD', 'Text', '-', '', 'Date time', 'Image date', 'HHMM', 'Text', '-', '', 'Session number', '1', 'Three digits', 'Text', '-iso', '', 'Metadata', 'ISO', '', 'Text', '-f', '', 'Metadata', 'Aperture', '', 'Text', '-', '', 'Metadata', 'Focal length', '', 'Text', 'mm-', '', 'Metadata', 'Exposure time', '', 'Filename', 'Extension', 'lowercase'] self.prefs.image_rename = r def checkPreferencesOnStartup(self): prefsOk = rn.checkPreferencesForValidity(self.prefs.image_rename, self.prefs.subfolder) if not prefsOk: title = PROGRAM_NAME msg = _("There is an error in the program preferences.") msg += " " + _("Some preferences will be reset.") sys.stderr.write(msg +'\n') # misc.run_dialog(title, msg) return prefsOk def needJobCode(self): return rn.usesJobCode(self.prefs.image_rename) or rn.usesJobCode(self.prefs.subfolder) def assignJobCode(self, code): """ assign job code (which may be empty) to global variable and update user preferences Update preferences only if code is not empty. Do not duplicate job code. """ global job_code if code == None: code = '' job_code = code if job_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 getUseDevice(self, path, volume, autostart): """ Prompt user whether or not to download from this device """ cmd_line(_("Prompting whether to use %s" % volume.get_name(limit=0))) d = UseDeviceDialog(self.widget, path, volume, autostart, self.gotUseDevice) def gotUseDevice(self, dialog, userSelected, permanent_choice, path, volume, autostart): """ User has chosen whether or not to use a device to download from """ dialog.destroy() if userSelected: if permanent_choice and path not in self.prefs.device_whitelist: # do not do a list append operation here without the assignment, or the 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] self.initiateScan(path, volume, autostart) 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 _getJobCode(self, postJobCodeEntryCB, autoStart): """ prompt for a job code """ if not self.prompting_for_job_code: cmd_line(_("Prompting for Job Code")) self.prompting_for_job_code = True j = JobCodeDialog(self.widget, self.prefs.job_codes, self.last_chosen_job_code, postJobCodeEntryCB, autoStart) else: cmd_line(_("Already prompting for Job Code, do not prompt again")) def getJobCode(self, autoStart=True): """ called from the copyphotos thread""" self._getJobCode(self.gotJobCode, autoStart) def gotJobCode(self, dialog, userChoseCode, code, autoStart): dialog.destroy() self.prompting_for_job_code = False if userChoseCode: self.assignJobCode(code) self.last_chosen_job_code = code if autoStart: cmd_line(_("Starting downloads that have been waiting for a Job Code")) for w in workers.getWaitingForJobCodeWorkers(): w.startStop() else: cmd_line(_("Starting downloads")) self.startDownload() # FIXME: what happens to these workers that are waiting? How will the user start their download? # check if need to add code to start button def checkForUpgrade(self, runningVersion): """ Checks if the running version of the program is different from the version recorded in the preferences. If the version is different, then the preferences are checked to see whether they should be upgraded or not. returns True if program preferences window should be opened """ displayPrefs = upgraded = False previousVersion = self.prefs.program_version if previousVersion: # the program has been run previously for this user pv = common.pythonifyVersion(previousVersion) rv = common.pythonifyVersion(runningVersion) title = PROGRAM_NAME imageRename = subfolder = None if pv != rv: if pv > rv: prefsOk = rn.checkPreferencesForValidity(self.prefs.image_rename, self.prefs.subfolder) msg = _("A newer version of this program was previously run on this computer.\n\n") if prefsOk: msg += _("Program preferences appear to be valid, but please check them to ensure correct operation.") else: msg += _("Sorry, some preferences are invalid and will be reset.") sys.stderr.write(_("Warning:") + " %s\n" % msg) misc.run_dialog(title, msg) displayPrefs = True else: cmd_line(_("This version of the program is newer than the previously run version. Checking preferences.")) # if True: if rn.checkPreferencesForValidity(self.prefs.image_rename, self.prefs.subfolder, previousVersion): upgraded, imageRename, subfolder = rn.upgradePreferencesToCurrent(self.prefs.image_rename, self.prefs.subfolder, previousVersion) if upgraded: self.prefs.image_rename = imageRename self.prefs.subfolder = subfolder cmd_line(_("Preferences were modified.")) msg = _('This version of the program uses different preferences than the old version. Your preferences have been updated.\n\nPlease check them to ensure correct operation.') misc.run_dialog(title, msg) displayPrefs = True else: cmd_line(_("No preferences needed to be changed.")) else: msg = _('This version of the program uses different preferences than the old version. Some of your previous preferences were invalid, and could not be updated. They will be reset.') sys.stderr.write(msg + "\n") misc.run_dialog(title, msg) displayPrefs = True return displayPrefs def initPyNotify(self): if not pynotify.init("TestCaps"): sys.stderr.write(_("Problem using pynotify.") + "\n") sys.exit(1) capabilities = {'actions': False, 'body': False, 'body-hyperlinks': False, 'body-images': False, 'body-markup': False, 'icon-multi': False, 'icon-static': False, 'sound': False, 'image/svg+xml': False, 'append': False} caps = pynotify.get_server_caps () if caps is None: sys.stderr.write(_("Failed to receive pynotify server capabilities.") + "\n") sys.exit (1) for cap in caps: capabilities[cap] = True info = pynotify.get_server_info() def usingVolumeMonitor(self): """ Returns True if programs needs to use gio or gnomevfs volume monitor """ return (self.prefs.device_autodetection or (self.prefs.backup_images and self.prefs.backup_device_autodetection )) def startVolumeMonitor(self): if not self.volumeMonitor: self.volumeMonitor = VMonitor(self) def displayBackupVolumes(self): """ Create a message to be displayed to the user showing which backup volumes will be used """ message = '' paths = self.backupVolumes.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.backupVolumes[b].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 searchForPsd(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 isGProxyShadowMount(self, gvfsVolume): """ gvfs GProxyShadowMount are used for camera specific things, not the data in the memory card """ if using_gio: #FIXME: this is a hack, but what is the correct function? return str(type(gvfsVolume)).find('GProxyShadowMount') >= 0 else: return False def workerHasThisPath(self, path): havePath= False for w in workers.getNonFinishedWorkers(): if w.cardMedia.path == path: havePath = True break return havePath def on_volume_mounted(self, monitor, mount): """ callback run when gnomevfs indicates a new volume has been mounted """ if self.usingVolumeMonitor(): volume = Volume(mount) path = volume.get_path() if path in self.prefs.device_blacklist and self.searchForPsd(): cmd_line(_("Device %(device)s (%(path)s) ignored") % { 'device': volume.get_name(limit=0), 'path': path}) else: if not self.isGProxyShadowMount(mount): self._printDetectedDevice(volume.get_name(limit=0), path) isBackupVolume = self.checkIfBackupVolume(path) if isBackupVolume: backupPath = os.path.join(path, self.prefs.backup_identifier) if path not in self.backupVolumes: self.backupVolumes[backupPath] = volume self.rapid_statusbar.push(self.statusbar_context_id, self.displayBackupVolumes()) elif media.isImageMedia(path) or self.searchForPsd(): if self.searchForPsd() and path not in self.prefs.device_whitelist: # prompt user if device should be used or not self.getUseDevice(path, volume, self.prefs.auto_download_upon_device_insertion) else: self._printAutoStart(self.prefs.auto_download_upon_device_insertion) self.initiateScan(path, volume, self.prefs.auto_download_upon_device_insertion) def initiateScan(self, path, volume, autostart): """ initiates scan of image device""" cardMedia = CardMedia(path, volume, True) i = workers.getNextThread_id() workers.append(CopyPhotos(i, self, self.fileRenameLock, self.fileSequenceLock, self.statsLock, self.downloadStats, autostart, cardMedia)) self.setDownloadButtonSensitivity() self.startScan() def on_volume_unmounted(self, monitor, volume): """ callback run when gnomevfs indicates a volume has been unmounted """ volume = Volume(volume) path = volume.get_path() # four scenarios - # volume is waiting to be scanned # the volume has been scanned but downloading has not yet started # images are being downloaded from volume (it must be a messy unmount) # images finished downloading from volume if path: # first scenario for w in workers.getReadyToStartWorkers(): if w.cardMedia.volume == volume: media_collection_treeview.removeCard(w.thread_id) workers.disableWorker(w.thread_id) # second scenario for w in workers.getReadyToDownloadWorkers(): if w.cardMedia.volume == volume: media_collection_treeview.removeCard(w.thread_id) workers.disableWorker(w.thread_id) # fourth scenario - nothing to do # remove backup volumes backupPath = os.path.join(path, self.prefs.backup_identifier) if backupPath in self.backupVolumes: del self.backupVolumes[backupPath] self.rapid_statusbar.push(self.statusbar_context_id, self.displayBackupVolumes()) def clearCompletedDownloads(self): """ clears the display of completed downloads """ for w in workers.getFinishedWorkers(): media_collection_treeview.removeCard(w.thread_id) def clearNotStartedDownloads(self): """ Clears the display of the download and instructs the thread not to run """ for w in workers.getNotDownloadingWorkers(): media_collection_treeview.removeCard(w.thread_id) workers.disableWorker(w.thread_id) def checkIfBackupVolume(self, path): """ Checks to see if backups are enabled and path represents a valid backup location Checks against user preferences. """ if self.prefs.backup_images: if self.prefs.backup_device_autodetection: if media.isBackupMedia(path, self.prefs.backup_identifier): return True elif path == self.prefs.backup_location: # user manually specified the path return True return False def _printDetectedDevice(self, volume_name, path): cmd_line (_("Detected %(device)s with path %(path)s") % {'device': volume_name, 'path': path}) def _printAutoStart(self, autoStart): if autoStart: cmd_line(_("Automatically start download is true") ) else: cmd_line(_("Automatically start download is false") ) def setupAvailableImageAndBackupMedia(self, onStartup, onPreferenceChange, doNotAllowAutoStart): """ Sets up volumes for downloading from and backing up to onStartup should be True if the program is still starting, i.e. this is being called from the program's initialization. onPreferenceChange should be True if this is being called as the result of a preference bring changed Removes any image media that are currently not downloaded, or finished downloading """ self.clearNotStartedDownloads() volumeList = [] self.backupVolumes = {} if not workers.noDownloadingWorkers(): self.downloadStats.clear() self._resetDownloadInfo() if self.usingVolumeMonitor(): # either using automatically detected backup devices # or image devices for v in self.volumeMonitor.get_mounts(): volume = Volume(v) path = volume.get_path(avoid_gnomeVFS_bug = True) if path: if path in self.prefs.device_blacklist and self.searchForPsd(): cmd_line(_("Device %(device)s (%(path)s) ignored") % { 'device': volume.get_name(limit=0), 'path': path}) else: if not self.isGProxyShadowMount(v): self._printDetectedDevice(volume.get_name(limit=0), path) isBackupVolume = self.checkIfBackupVolume(path) if isBackupVolume: backupPath = os.path.join(path, self.prefs.backup_identifier) self.backupVolumes[backupPath] = volume elif self.prefs.device_autodetection and (media.isImageMedia(path) or self.searchForPsd()): volumeList.append((path, volume)) if not self.prefs.device_autodetection: # user manually specified the path from which to download images path = self.prefs.device_location if path: cmd_line(_("Using manually specified path") + " %s" % path) volumeList.append((path, None)) if self.prefs.backup_images: if not self.prefs.backup_device_autodetection: # user manually specified backup location self.backupVolumes[self.prefs.backup_location] = None self.rapid_statusbar.push(self.statusbar_context_id, '') else: self.rapid_statusbar.push(self.statusbar_context_id, self.displayBackupVolumes()) else: self.rapid_statusbar.push(self.statusbar_context_id, '') # add each memory card / other device to the list of threads if doNotAllowAutoStart: autoStart = False else: autoStart = (not onPreferenceChange) and ((self.prefs.auto_download_at_startup and onStartup) or (self.prefs.auto_download_upon_device_insertion and not onStartup)) self._printAutoStart(autoStart) for i in range(len(volumeList)): path, volume = volumeList[i] if self.searchForPsd() and path not in self.prefs.device_whitelist: # prompt user to see if device should be used or not self.getUseDevice(path, volume, autoStart) else: self.initiateScan(path, volume, autoStart) def _setupDownloadbutton(self): self.download_hbutton_box = gtk.HButtonBox() self.download_button_is_download = True self.download_button = gtk.Button() self.download_button.set_use_underline(True) self.download_button.set_flags(gtk.CAN_DEFAULT) self._set_download_button() self.download_button.connect('clicked', self.on_download_button_clicked) self.download_hbutton_box.set_layout(gtk.BUTTONBOX_START) self.download_hbutton_box.pack_start(self.download_button) self.download_hbutton_box.show_all() self.buttons_hbox.pack_start(self.download_hbutton_box, padding=hd.WINDOW_BORDER_SPACE) self.setDownloadButtonSensitivity() def set_display_thumbnails(self, value): if value: self.image_scrolledwindow.show_all() else: self.image_scrolledwindow.hide() def _resetDownloadInfo(self): self.markSet = False self.startTime = None self.totalDownloadSize = self.totalDownloadedSoFar = 0 self.totalDownloadSizeThisRun = self.totalDownloadedSoFarThisRun = 0 # there is no need to clear self.timeRemaining, as when each thread is completed, it removes itself global job_code job_code = None def addToTotalDownloadSize(self, size): self.totalDownloadSize += size def setOverallDownloadMark(self): if not self.markSet: self.markSet = True self.totalDownloadSizeThisRun = self.totalDownloadSize - self.totalDownloadedSoFar self.totalDownloadedSoFarThisRun = 0 self.startTime = time.time() self.timeStatusBarUpdated = self.startTime self.timeMark = self.startTime self.sizeMark = 0 def startOrResumeWorkers(self): # resume any paused workers for w in workers.getPausedDownloadingWorkers(): w.startStop() self.timeRemaining.setTimeMark(w) #start any new workers workers.startDownloadingWorkers() if is_beta and verbose: workers.printWorkerStatus() def updateOverallProgress(self, thread_id, imageSize, percentComplete): """ Updates progress bar and status bar text with time remaining to download images """ self.totalDownloadedSoFar += imageSize self.totalDownloadedSoFarThisRun += imageSize fraction = self.totalDownloadedSoFar / float(self.totalDownloadSize) self.download_progressbar.set_fraction(fraction) if percentComplete == 100.0: self.menu_clear.set_sensitive(True) self.timeRemaining.remove(thread_id) if self.downloadComplete(): # finished all downloads self.rapid_statusbar.push(self.statusbar_context_id, "") self.download_button_is_download = True self._set_download_button() self.setDownloadButtonSensitivity() cmd_line(_("All downloads complete")) if is_beta and verbose: workers.printWorkerStatus() else: now = time.time() self.timeRemaining.update(thread_id, imageSize) if now > (self.downloadTimeGap + self.timeMark): amtTime = now - self.timeMark self.timeMark = now amtDownloaded = self.totalDownloadedSoFarThisRun - self.sizeMark self.sizeMark = self.totalDownloadedSoFarThisRun amtToDownload = float(self.totalDownloadSizeThisRun) - self.totalDownloadedSoFarThisRun downloadSpeed = "%1.1f" % (amtDownloaded / 1048576 / amtTime) +_("MB/s") self.speed_label.set_text(downloadSpeed) timeRemaining = self.timeRemaining.timeRemaining() if timeRemaining: secs = int(timeRemaining) 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.push(self.statusbar_context_id, message) def resetSequences(self): if self.downloadComplete(): sequences.reset(self.prefs.getDownloadsToday(), self.prefs.stored_sequence_no) def notifyUserAllDownloadsComplete(self): """ Possibly notify the user all downloads are complete using libnotify Reset progress bar info""" if self.downloadComplete(): if self.displayDownloadSummaryNotification: message = _("All downloads complete") message += "\n%s " % self.downloadStats.noImagesDownloaded + _("images downloaded") if self.downloadStats.noImagesSkipped: message = "%s\n%s " % (message, self.downloadStats.noImagesSkipped) + _("images skipped") if self.downloadStats.noWarnings: message = "%s\n%s " % (message, self.downloadStats.noWarnings) + _("warnings") if self.downloadStats.noErrors: message = "%s\n%s " % (message, self.downloadStats.noErrors) +_("errors") n = pynotify.Notification(PROGRAM_NAME, message) n.show() self.displayDownloadSummaryNotification = False # don't show it again unless needed self.downloadStats.clear() self._resetDownloadInfo() self.speed_label.set_text(' ') def exitOnDownloadComplete(self): if self.downloadComplete(): if self.prefs.auto_exit: if not (self.downloadStats.noErrors or self.downloadStats.noWarnings): self.quit() def downloadComplete(self): return self.totalDownloadedSoFar == self.totalDownloadSize def setDownloadButtonSensitivity(self): isSensitive = workers.noReadyToDownloadWorkers() > 0 or workers.noDownloadingWorkers() > 0 if isSensitive: self.download_button.props.sensitive = True self.menu_download_pause.props.sensitive = True else: self.download_button.props.sensitive = False self.menu_download_pause.props.sensitive = False return isSensitive def on_rapidapp_destroy(self, widget): """Called when the application is going to quit""" workers.quitAllWorkers() self.flushevents() display_queue.close("w") def on_menu_clear_activate(self, widget): self.clearCompletedDownloads() widget.set_sensitive(False) def on_menu_report_problem_activate(self, widget): webbrowser.open("https://bugs.launchpad.net/rapid") def on_menu_get_help_online_activate(self, widget): webbrowser.open("http://www.damonlynch.net/rapid/help.html") def on_menu_donate_activate(self, widget): webbrowser.open("http://www.damonlynch.net/rapid/donate.html") def on_menu_translate_activate(self, widget): webbrowser.open("http://www.damonlynch.net/rapid/translate.html") def on_menu_preferences_activate(self, widget): """ Sets preferences for the application using dialog window """ PreferencesDialog(self) def on_menu_log_window_toggled(self, widget): active = widget.get_active() self.prefs.show_log_dialog = active if active: log_dialog.widget.show() else: log_dialog.widget.hide() def on_menu_display_thumbnails_toggled(self, check_button): self.prefs.display_thumbnails = check_button.get_active() def on_menu_about_activate(self, widget): """ Display about dialog box """ about = gtk.glade.XML(paths.share_dir(config.GLADE_FILE), "about").get_widget("about") about.set_property("name", PROGRAM_NAME) about.set_property("version", __version__) about.run() about.destroy() def _set_download_button(self): """ Sets download button to appropriate state """ if self.download_button_is_download: # This text will be displayed to the user on the Download / Pause button. # Please note the space at the end of the label - it is needed to meet the Gnome Human Interface Guidelines self.download_button.set_label(_("_Download ")) self.download_button.set_image(gtk.image_new_from_stock( gtk.STOCK_CONVERT, gtk.ICON_SIZE_BUTTON)) else: # button should indicate paused state self.download_button.set_image(gtk.image_new_from_stock( gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_BUTTON)) # This text will be displayed to the user on the Download / Pause button. self.download_button.set_label(_("_Pause") + " ") def on_menu_download_pause_activate(self, widget): self.on_download_button_clicked(widget) def startScan(self): if workers.noReadyToStartWorkers() > 0: workers.startWorkers() def postStartDownloadTasks(self): if workers.noDownloadingWorkers() > 1: self.displayDownloadSummaryNotification = True # set button to display Pause self.download_button_is_download = False self._set_download_button() def startDownload(self): self.startOrResumeWorkers() self.postStartDownloadTasks() def pauseDownload(self): for w in workers.getDownloadingWorkers(): w.startStop() # set button to display Download if not self.download_button_is_download: self.download_button_is_download = True self._set_download_button() def on_download_button_clicked(self, widget): """ Handle download button click. Button is in one of two states: download, or pause. If download, a click indicates to start or resume a download run. If pause, a click indicates to pause all running downloads. """ if self.download_button_is_download: if need_job_code and job_code == None and not self.prompting_for_job_code: self.getJobCode(autoStart=False) else: self.startDownload() else: self.pauseDownload() def on_preference_changed(self, key, value): """ Called when user changes the program's preferences """ if key == 'display_thumbnails': self.set_display_thumbnails(value) elif key == 'show_log_dialog': self.menu_log_window.set_active(value) elif key in ['device_autodetection', 'device_autodetection_psd', 'backup_images', 'device_location', 'backup_device_autodetection', 'backup_location' ]: if self.usingVolumeMonitor(): self.startVolumeMonitor() cmd_line("\n" + _("Preferences were changed.")) self.setupAvailableImageAndBackupMedia(onStartup = False, onPreferenceChange = True, doNotAllowAutoStart = False) if is_beta and verbose: print "Current worker status:" workers.printWorkerStatus() elif key in ['subfolder', 'image_rename']: global need_job_code need_job_code = self.needJobCode() def on_error_eventbox_button_press_event(self, widget, event): self.prefs.show_log_dialog = True log_dialog.widget.show() class VMonitor: """ Transistion to gvfs from gnomevfs""" def __init__(self, app): self.app = app if using_gio: self.vmonitor = gio.volume_monitor_get() self.vmonitor.connect("mount-added", self.app.on_volume_mounted) self.vmonitor.connect("mount-removed", self.app.on_volume_unmounted) else: self.vmonitor = gnomevfs.VolumeMonitor() self.vmonitor.connect("volume-mounted", self.app.on_volume_mounted) self.vmonitor.connect("volume-unmounted", self.app.on_volume_unmounted) def get_mounts(self): if using_gio: return self.vmonitor.get_mounts() else: return self.vmonitor.get_mounted_volumes() class Volume: """ Transistion to gvfs from gnomevfs""" def __init__(self, volume): self.volume = volume def get_name(self, limit=config.MAX_LENGTH_DEVICE_NAME): if using_gio: v = self.volume.get_name() else: v = self.volume.get_display_name() if limit: if len(v) > limit: v = v[:limit] + '...' return v def get_path(self, avoid_gnomeVFS_bug = False): if using_gio: path = self.volume.get_root().get_path() else: uri = self.volume.get_activation_uri() path = None if avoid_gnomeVFS_bug: # ugly hack to work around bug where gnomevfs.get_local_path_from_uri(uri) causes a crash mediaLocation = "file://" + config.MEDIA_LOCATION if uri.find(mediaLocation) == 0: path = gnomevfs.get_local_path_from_uri(uri) else: path = gnomevfs.get_local_path_from_uri(uri) return path def get_icon_pixbuf(self, size): """ returns icon for the volume, or None if not available""" icontheme = gtk.icon_theme_get_default() if using_gio: gicon = self.volume.get_icon() f = None if isinstance(gicon, gio.ThemedIcon): iconinfo = icontheme.choose_icon(gicon.get_names(), size, gtk.ICON_LOOKUP_USE_BUILTIN) f = iconinfo.get_filename() try: v = gtk.gdk.pixbuf_new_from_file_at_size(f, size, size) except: f = None if not f: v = icontheme.load_icon('gtk-harddisk', size, gtk.ICON_LOOKUP_USE_BUILTIN) else: gicon = self.volume.get_icon() v = icontheme.load_icon(gicon, size, gtk.ICON_LOOKUP_USE_BUILTIN) return v def unmount(self, callback): self.volume.unmount(callback) class DownloadStats: def __init__(self): self.clear() def adjust(self, size, noImagesDownloaded, noImagesSkipped, noWarnings, noErrors): self.downloadSize += size self.noImagesDownloaded += noImagesDownloaded self.noImagesSkipped += noImagesSkipped self.noWarnings += noWarnings self.noErrors += noErrors def clear(self): self.noImagesDownloaded = self.noImagesSkipped = 0 self.downloadSize = 0 self.noWarnings = self.noErrors = 0 class TimeForDownload: pass class TimeRemaining: gap = 2 def __init__(self): self.clear() def add(self, w, size): if w not in self.times: t = TimeForDownload() t.timeRemaining = None t.size = size t.downloaded = 0 t.sizeMark = 0 t.timeMark = time.time() self.times[w] = t def update(self, w, size): if w in self.times: self.times[w].downloaded += size now = time.time() tm = self.times[w].timeMark amtTime = now - tm if amtTime > self.gap: self.times[w].timeMark = now amtDownloaded = self.times[w].downloaded - self.times[w].sizeMark self.times[w].sizeMark = self.times[w].downloaded timefraction = amtDownloaded / amtTime amtToDownload = float(self.times[w].size) - self.times[w].downloaded self.times[w].timeRemaining = amtToDownload / timefraction def _timeEstimates(self): for t in self.times: yield self.times[t].timeRemaining def timeRemaining(self): return max(self._timeEstimates()) def setTimeMark(self, w): if w in self.times: self.times[w].timeMark = time.time() def clear(self): self.times = {} def remove(self, w): if w in self.times: del self.times[w] def programStatus(): print _("Goodbye") def start (): global is_beta is_beta = config.version.find('~b') > 0 parser = OptionParser(version= "%%prog %s" % 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("-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 image file extensions the program recognizes and exit")) (options, args) = parser.parse_args() global verbose verbose = options.verbose if verbose: atexit.register(programStatus) if options.extensions: exts = config.RAW_FILE_EXTENSIONS + config.NON_RAW_IMAGE_FILE_EXTENSIONS v = '' for e in exts[:-1]: v += '%s, ' % e.upper() v = v[:-1] + ' '+ (_('and %s') % exts[-1].upper()) print v sys.exit(0) if using_gio: cmd_line(_("Using") + " GIO") else: # Which volume management code is being used (GIO or GnomeVFS) cmd_line(_("Using") + " GnomeVFS") gdk.threads_init() display_queue.open("rw") tube.tube_add_watch(display_queue, updateDisplay) gdk.threads_enter() # run only a single instance of the application 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) else: # this application is already running print _("%s is already running") % PROGRAM_NAME object = bus.get_object (config.DBUS_NAME, "/") app = dbus.Interface (object, config.DBUS_NAME) app.start() gdk.threads_leave() if __name__ == "__main__": start()