diff options
Diffstat (limited to 'rapid/rapid.py')
-rwxr-xr-x | rapid/rapid.py | 8698 |
1 files changed, 2744 insertions, 5954 deletions
diff --git a/rapid/rapid.py b/rapid/rapid.py index f41c79f..335db34 100755 --- a/rapid/rapid.py +++ b/rapid/rapid.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: latin1 -*- -### Copyright (C) 2007, 2008, 2009, 2010 Damon Lynch <damonlynch@gmail.com> +### Copyright (C) 2011 Damon Lynch <damonlynch@gmail.com> ### This program is free software; you can redistribute it and/or modify ### it under the terms of the GNU General Public License as published by @@ -17,19 +17,8 @@ ### 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 types -import webbrowser -import operator import dbus import dbus.bus @@ -37,1549 +26,91 @@ 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 +from optparse import OptionParser +import gtk import gtk.gdk as gdk -import pango -import gobject - -try: - import gio - import glib - using_gio = True -except ImportError: - import gnomevfs - using_gio = False - -import prefs -import paths -import gnomeglade -from optparse import OptionParser +import webbrowser + +import sys, time, types, os, datetime +import gobject, pango, cairo, array, pangocairo, gio import pynotify -import idletube as tube +from multiprocessing import Process, Pipe, Queue, Event, Value, Array, current_process, log_to_stderr +from ctypes import c_int, c_bool, c_char -import config +import logging +logger = log_to_stderr() -from config import STATUS_CANNOT_DOWNLOAD, STATUS_DOWNLOADED, \ - STATUS_DOWNLOADED_WITH_WARNING, \ - STATUS_DOWNLOAD_FAILED, \ - STATUS_DOWNLOAD_PENDING, \ - STATUS_BACKUP_PROBLEM, \ - STATUS_NOT_DOWNLOADED, \ - STATUS_DOWNLOAD_AND_BACKUP_FAILED, \ - STATUS_WARNING - -import common -import misc -import higdefaults as hd +# Rapid Photo Downloader modules -from media import getDefaultPhotoLocation, getDefaultVideoLocation, \ - getDefaultBackupPhotoIdentifier, \ - getDefaultBackupVideoIdentifier +import rpdfile -import ValidatedEntry - -from media import CardMedia - -import media - -import metadata -import videometadata -from videometadata import DOWNLOAD_VIDEO - -import renamesubfolderprefs as rn import problemnotification as pn +import thumbnail as tn +import rpdmultiprocessing as rpdmp -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) - -try: - from dropshadow import image_to_pixbuf, pixbuf_to_image, DropShadow - DROP_SHADOW = True -except: - DROP_SHADOW = False - - -#~ DROP_SHADOW = False # for testing - -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') - -TINY_SCREEN = gtk.gdk.screen_height() <= config.TINY_SCREEN_HEIGHT -#~ TINY_SCREEN = True - -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 = selection_hbox = log_dialog = None - -job_code = None -need_job_code_for_renaming = False - -class ThreadManager: - """ - Manages the threads that actually download photos and videos - """ - _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 _isScanning(self, w): - return w.isAlive() and w.hasStarted and not w.scanComplete and not w.manuallyDisabled - - def _isDownloading(self, w): - return w.downloadStarted and w.isAlive() and not w.downloadComplete - - def _isPaused(self, w): - return w.downloadStarted and not w.running and not w.downloadComplete and not w.manuallyDisabled and w.isAlive() - - 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(): - #for some reason, very occassionally a thread that has been started shows up in this list, so must filter them out - if not w.isAlive(): - w.start() - - 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 getNotDownloadingAndNotFinishedWorkers(self): - for w in self._workers: - if w.hasStarted and not w.downloadStarted and not self._isFinished(w): - yield w - - - def noReadyToStartWorkers(self): - n = 0 - for w in self._workers: - if self._isReadyToStart(w): - n += 1 - return n - - def noScanningWorkers(self): - n = 0 - for w in self._workers: - if self._isScanning(w): - n += 1 - return n - - def getScanningWorkers(self): - for w in self._workers: - if self._isScanning(w): - yield w - - def scanComplete(self, threads): - """ - Returns True only if the list of threads have completed their scan - """ - for thread_id in threads: - if not self[thread_id].scanComplete: - return False - return True - - 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 getPausedDownloadingWorkers(self): - for w in self._workers: - if self._isPaused(w): - yield w - - def getWaitingForJobCodeWorkers(self): - for w in self._workers: - if w.waitingForJobCode: - yield w - - def getAutoStartWorkers(self): - for w in self._workers: - if w.autoStart: - 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 +import preferencesdialog +import prefsrapid - def noRunningWorkers(self): - i = 0 - for w in self._workers: - if w.hasStarted and w.isAlive(): - i += 1 - return i - - def noPausedWorkers(self): - i = 0 - for w in self._workers: - if self._isPaused(w): - 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): - if TINY_SCREEN: - zoom = 120 - else: - zoom = config.MIN_THUMBNAIL_SIZE * 2 - - defaults = { - "program_version": prefs.Value(prefs.STRING, ""), - "download_folder": prefs.Value(prefs.STRING, - getDefaultPhotoLocation()), - "video_download_folder": prefs.Value(prefs.STRING, - getDefaultVideoLocation()), - "subfolder": prefs.ListValue(prefs.STRING_LIST, rn.DEFAULT_SUBFOLDER_PREFS), - "video_subfolder": prefs.ListValue(prefs.STRING_LIST, rn.DEFAULT_VIDEO_SUBFOLDER_PREFS), - "image_rename": prefs.ListValue(prefs.STRING_LIST, [rn.FILENAME, - rn.NAME_EXTENSION, - rn.ORIGINAL_CASE]), - "video_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, - getDefaultBackupPhotoIdentifier()), - "video_backup_identifier": prefs.Value(prefs.STRING, - getDefaultBackupVideoIdentifier()), - "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), - "auto_delete": prefs.Value(prefs.BOOL, False), - "download_conflict_resolution": prefs.Value(prefs.STRING, - config.SKIP_DOWNLOAD), - "backup_duplicate_overwrite": prefs.Value(prefs.BOOL, False), - "display_selection": prefs.Value(prefs.BOOL, True), - "display_size_column": prefs.Value(prefs.BOOL, True), - "display_filename_column": prefs.Value(prefs.BOOL, False), - "display_type_column": prefs.Value(prefs.BOOL, True), - "display_path_column": prefs.Value(prefs.BOOL, False), - "display_device_column": prefs.Value(prefs.BOOL, False), - "display_preview_folders": 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')]), - "synchronize_raw_jpg": prefs.Value(prefs.BOOL, False), - "hpaned_pos": prefs.Value(prefs.INT, 0), - "vpaned_pos": prefs.Value(prefs.INT, 0), - "main_window_size_x": prefs.Value(prefs.INT, 0), - "main_window_size_y": prefs.Value(prefs.INT, 0), - "main_window_maximized": prefs.Value(prefs.INT, 0), - "show_warning_downloading_from_camera": prefs.Value(prefs.BOOL, True), - "preview_zoom": prefs.Value(prefs.INT, zoom), - } - - 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 '' - - def reset(self): - """ - resets all preferences to default values - """ - - prefs.Preferences.reset(self) - self.program_version = __version__ - -class ImageRenameTable(tpm.TablePlusMinus): - - def __init__(self, parentApp, adjustScrollWindow): - - tpm.TablePlusMinus.__init__(self, 1, 3) - self.parentApp = parentApp - self.adjustScrollWindow = adjustScrollWindow - if not hasattr(self, "errorTitle"): - self.errorTitle = _("Error in Photo Rename preferences") - - self.table_type = self.errorTitle[len("Error in "):] - self.i = 0 - - if adjustScrollWindow: - self.scrollBar = self.adjustScrollWindow.get_vscrollbar() - #this next line does not work on early versions of pygtk :( - self.scrollBar.connect('visibility-notify-event', self.scrollbar_visibility_change) - self.connect("size-request", self.size_adjustment) - self.connect("add", self.size_adjustment) - self.connect("remove", self.size_adjustment) - - # get scrollbar thickness from parent app scrollbar - very hackish, but what to do?? - self.bump = 16# self.parentApp.parentApp.image_scrolledwindow.get_hscrollbar().allocation.height - self.haveVerticalScrollbar = False - - # 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() - self.prefsFactory.setDownloadStartTime(datetime.datetime.now()) - - 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." + "\n") - 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 scrollbar_visibility_change(self, widget, event): - if event.state == gdk.VISIBILITY_UNOBSCURED: - self.haveVerticalScrollbar = True - self.adjustScrollWindow.set_size_request(self.adjustScrollWindow.allocation.width + self.bump, -1) - - - def size_adjustment(self, widget, arg2): - """ - Adjust scrolledwindow width in preferences dialog to reflect width of image rename table - - The algorithm is complicated by the need to take into account the presence of a vertical scrollbar, - which might be added as the user adds more rows - - The pygtk code behaves inconsistently depending on the pygtk version - """ - - if self.adjustScrollWindow: - self.haveVerticalScrollbar = self.scrollBar.allocation.width > 1 or self.haveVerticalScrollbar - if not self.haveVerticalScrollbar: - if self.allocation.width > self.adjustScrollWindow.allocation.width: - self.adjustScrollWindow.set_size_request(self.allocation.width, -1) - else: - if self.allocation.width > self.adjustScrollWindow.allocation.width - self.bump: - self.adjustScrollWindow.set_size_request(self.allocation.width + self.bump, -1) - self.bump = 0 - - 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 VideoRenameTable(ImageRenameTable): - def __init__(self, parentApp, adjustScollWindow): - self.errorTitle = _("Error in Video Rename preferences") - ImageRenameTable.__init__(self, parentApp, adjustScollWindow) - - def getParentAppPrefs(self): - self.prefList = self.parentApp.prefs.video_rename - - def getPrefsFactory(self): - self.prefsFactory = rn.VideoRenamePreferences(self.prefList, self, - sequences = sequences) - - def updateParentAppPrefs(self): - self.parentApp.prefs.video_rename = self.prefList +import tableplusminus as tpm +import generatename as gn - def updateExample(self): - self.parentApp.updateVideoRenameExample() +import downloadtracker -class SubfolderTable(ImageRenameTable): - def __init__(self, parentApp, adjustScollWindow): - self.errorTitle = _("Error in Photo Download Subfolders preferences") - ImageRenameTable.__init__(self, parentApp, adjustScollWindow) +from metadatavideo import DOWNLOAD_VIDEO +import metadataphoto +import metadatavideo - 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 +import scan as scan_process +import copyfiles +import subfolderfile +import backupfile - def updateExample(self): - self.parentApp.updatePhotoDownloadFolderExample() - -class VideoSubfolderTable(ImageRenameTable): - def __init__(self, parentApp, adjustScollWindow): - self.errorTitle = _("Error in Video Download Subfolders preferences") - ImageRenameTable.__init__(self, parentApp, adjustScollWindow) - - def getParentAppPrefs(self): - self.prefList = self.parentApp.prefs.video_subfolder - - def getPrefsFactory(self): - self.prefsFactory = rn.VideoSubfolderPreferences(self.prefList, self) - - def updateParentAppPrefs(self): - self.parentApp.prefs.video_subfolder = self.prefList +import errorlog - def updateExample(self): - self.parentApp.updateVideoDownloadFolderExample() - -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 - - parentApp.preferencesDialogDisplayed = True - - self.parentApp = parentApp - - self._setupTabSelector() - - self._setupControlSpacing() - - if DOWNLOAD_VIDEO: - self.file_types = _("photos and videos") - else: - self.file_types = _("photos") +import device as dv +import utilities - # get example photo and video data - try: - w = workers.firstWorkerReadyToDownload() - mediaFile = w.firstImage() - self.sampleImageName = mediaFile.name - # assume the metadata is already read - self.sampleImage = mediaFile.metadata - except: - self.sampleImage = metadata.DummyMetaData() - self.sampleImageName = 'IMG_0524.CR2' - - try: - mediaFile = w.firstVideo() - self.sampleVideoName = mediaFile.name - self.sampleVideo = mediaFile.metadata - self.videoFallBackDate = mediaFile.modificationTime - except: - self.sampleVideo = videometadata.DummyMetaData() - self.sampleVideoName = 'MVI_1379.MOV' - self.videoFallBackDate = datetime.datetime.now() - - - # setup tabs - self._setupPhotoDownloadFolderTab() - self._setupImageRenameTab() - self._setupVideoDownloadFolderTab() - self._setupVideoRenameTab() - self._setupRenameOptionsTab() - self._setupJobCodeTab() - self._setupDeviceTab() - self._setupBackupTab() - self._setupAutomationTab() - self._setupErrorTab() - - if not DOWNLOAD_VIDEO: - self.disableVideoControls() - - 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 photo 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.updatePhotoDownloadFolderExample() - - def on_video_download_folder_filechooser_button_selection_changed(self, widget): - self.prefs.video_download_folder = widget.get_current_folder() - self.updateVideoDownloadFolderExample() - - 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 - """ - - self._setupTableSpacing(self.download_folder_table) - self._setupTableSpacing(self.video_download_folder_table) - self.download_folder_table.set_row_spacing(2, - hd.VERTICAL_CONTROL_SPACE) - self.video_download_folder_table.set_row_spacing(2, - hd.VERTICAL_CONTROL_SPACE) - self._setupTableSpacing(self.rename_example_table) - self._setupTableSpacing(self.video_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) - - - 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 _setupVideoSubfolderTable(self): - self.video_subfolder_table = VideoSubfolderTable(self, None) - self.video_subfolder_vbox.pack_start(self.video_subfolder_table) - self.video_subfolder_table.show_all() - - def _setupPhotoDownloadFolderTab(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.updatePhotoDownloadFolderExample() - - def _setupVideoDownloadFolderTab(self): - self.video_download_folder_filechooser_button = gtk.FileChooserButton( - _("Select a folder to download videos to")) - self.video_download_folder_filechooser_button.set_current_folder( - self.prefs.video_download_folder) - self.video_download_folder_filechooser_button.set_action( - gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) - self.video_download_folder_filechooser_button.connect("selection-changed", - self.on_video_download_folder_filechooser_button_selection_changed) - - self.video_download_folder_table.attach( - self.video_download_folder_filechooser_button, - 2, 3, 2, 3, yoptions = gtk.SHRINK) - self.video_download_folder_filechooser_button.show() - self._setupVideoSubfolderTable() - self.updateVideoDownloadFolderExample() - - def _setupImageRenameTab(self): +import config +__version__ = config.version - 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("<i>%s</i>" % self.sampleImageName) - self.updateImageRenameExample() - - def _setupVideoRenameTab(self): +import paths - self.video_rename_table = VideoRenameTable(self, self.video_rename_scrolledwindow) - self.video_rename_table_vbox.pack_start(self.video_rename_table) - self.video_rename_table.show_all() - self.video_original_name_label.set_markup("<i>%s</i>" % self.sampleVideoName) - self.updateVideoRenameExample() - - 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)) - - self.synchronize_raw_jpg_checkbutton.set_active( - self.prefs.synchronize_raw_jpg) - - #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.remove_all_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 a folder containing %(file_types)s") % {'file_types':self.file_types}) - 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 %(file_types)s") % {'file_types':self.file_types}) - 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, 8, 9, yoptions = gtk.SHRINK) - self.backup_folder_filechooser_button.show() - self.backup_identifier_entry.set_text(self.prefs.backup_identifier) - self.video_backup_identifier_entry.set_text(self.prefs.video_backup_identifier) - - #setup controls for manipulating sensitivity - self._backupControls0 = [self.auto_detect_backup_checkbutton] - 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 - - self._backupVideoControls = [self.video_backup_identifier_label, - self.video_backup_identifier_entry] - - #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) - self.auto_delete_checkbutton.set_active( - self.prefs.auto_delete) - - - def _setupErrorTab(self): - 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_duplicate_overwrite: - self.backup_duplicate_overwrite_radiobutton.set_active(True) - else: - self.backup_duplicate_skip_radiobutton.set_active(True) +import gettext +gettext.bindtextdomain(config.APP_NAME) +gettext.textdomain(config.APP_NAME) - - def updateExampleFileName(self, display_table, rename_table, sample, sampleName, example_label, fallback_date = None): - problem = pn.Problem() - if hasattr(self, display_table): - rename_table.updateExampleJobCode() - rename_table.prefsFactory.initializeProblem(problem) - name = rename_table.prefsFactory.generateNameUsingPreferences( - sample, sampleName, - self.prefs.strip_characters, sequencesPreliminary=False, fallback_date=fallback_date) - else: - name = '' - - # since this is markup, escape it - text = "<i>%s</i>" % common.escape(name) - - if problem.has_problem(): - text += "\n" - # Translators: please do not modify or leave out html formatting tags like <i> and <b>. These are used to format the text the users sees - text += _("<i><b>Warning:</b> There is insufficient metadata to fully generate the name. Please use other renaming options.</i>") +from gettext import gettext as _ - example_label.set_markup(text) - - def updateImageRenameExample(self): - """ - Displays example image name to the user - """ - self.updateExampleFileName('rename_table', self.rename_table, self.sampleImage, self.sampleImageName, self.new_name_label) - - def updateVideoRenameExample(self): - """ - Displays example video name to the user - """ - self.updateExampleFileName('video_rename_table', self.video_rename_table, self.sampleVideo, self.sampleVideoName, self.video_new_name_label, self.videoFallBackDate) - - def updateDownloadFolderExample(self, display_table, subfolder_table, download_folder, sample, sampleName, example_download_path_label, subfolder_warning_label, fallback_date = None): - """ - Displays example subfolder name(s) to the user - """ - - problem = pn.Problem() - if hasattr(self, display_table): - subfolder_table.updateExampleJobCode() - subfolder_table.prefsFactory.initializeProblem(problem) - path = subfolder_table.prefsFactory.generateNameUsingPreferences( - sample, sampleName, - self.prefs.strip_characters, fallback_date = fallback_date) - else: - path = '' - - text = os.path.join(download_folder, path) - # since this is markup, escape it - path = common.escape(text) - if problem.has_problem(): - warning = _("<i><b>Warning:</b> There is insufficient metadata to fully generate subfolders. Please use other subfolder naming options.</i>" ) - else: - warning = "" - # Translators: you should not modify or leave out the %s. This is a code used by the programming language python to insert a value that thes user will see - example_download_path_label.set_markup(_("<i>Example: %s</i>") % text) - subfolder_warning_label.set_markup(warning) - - def updatePhotoDownloadFolderExample(self): - if hasattr(self, 'subfolder_table'): - self.updateDownloadFolderExample('subfolder_table', self.subfolder_table, self.prefs.download_folder, self.sampleImage, self.sampleImageName, self.example_photo_download_path_label, self.photo_subfolder_warning_label) - - def updateVideoDownloadFolderExample(self): - if hasattr(self, 'video_subfolder_table'): - self.updateDownloadFolderExample('video_subfolder_table', self.video_subfolder_table, self.prefs.video_download_folder, self.sampleVideo, self.sampleVideoName, self.example_video_download_path_label, self.video_subfolder_warning_label, self.videoFallBackDate) - - 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): - # do not update value if a download is occurring - it will mess it up! - if workers.noDownloadingWorkers() <> 0: - cmd_line(_("Downloads today value not updated, as a download is currently occurring")) - else: - 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): - # do not update value if a download is occurring - it will mess it up! - if workers.noDownloadingWorkers() <> 0: - cmd_line(_("Stored number value not updated, as a download is currently occurring")) - else: - 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() +from utilities import format_size_for_user +from utilities import register_iconsets - def _updateSubfolderPrefOnError(self, newPrefList): - self.prefs.subfolder = newPrefList - def _updateVideoSubfolderPrefOnError(self, newPrefList): - self.prefs.video_subfolder = newPrefList - - - def checkSubfolderValuesValidOnExit(self, usersPrefList, updatePrefFunction, filetype, defaultPrefList): - """ - Checks that the user has not entered in any inappropriate values - - If they have, filters out bad values and warns the user - """ - filtered, prefList = rn.filterSubfolderPreferences(usersPrefList) - if filtered: - cmd_line(_("The %(filetype)s subfolder preferences had some unnecessary values removed.") % {'filetype': filetype}) - if prefList: - updatePrefFunction(prefList) - else: - #Preferences list is now empty - msg = _("The %(filetype)s subfolder preferences entered are invalid and cannot be used.\nThey will be reset to their default values.") % {'filetype': filetype} - sys.stderr.write(msg + "\n") - misc.run_dialog(PROGRAM_NAME, msg) - updatePrefFunction(self.prefs.get_default(defaultPrefList)) - - 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") - self.prefs.video_backup_identifier = self.video_backup_identifier_entry.get_property("text") - - #check subfolder preferences for bad values - self.checkSubfolderValuesValidOnExit(self.prefs.subfolder, self._updateSubfolderPrefOnError, _("photo"), "subfolder") - self.checkSubfolderValuesValidOnExit(self.prefs.video_subfolder, self._updateVideoSubfolderPrefOnError, _("video"), "video_subfolder") +from config import STATUS_CANNOT_DOWNLOAD, STATUS_DOWNLOADED, \ + STATUS_DOWNLOADED_WITH_WARNING, \ + STATUS_DOWNLOAD_FAILED, \ + STATUS_DOWNLOAD_PENDING, \ + STATUS_BACKUP_PROBLEM, \ + STATUS_NOT_DOWNLOADED, \ + STATUS_DOWNLOAD_AND_BACKUP_FAILED, \ + STATUS_WARNING - self.widget.destroy() - self.parentApp.preferencesDialogDisplayed = False - self.parentApp.postPreferenceChange() - - - - - def on_add_job_code_button_clicked(self, button): - j = JobCodeDialog(self.widget, self.prefs.job_codes, None, self.add_job_code, False, True, True) - +DOWNLOADED = [STATUS_DOWNLOADED, STATUS_DOWNLOADED_WITH_WARNING, STATUS_BACKUP_PROBLEM] - def add_job_code(self, dialog, userChoseCode, job_code, autoStart, downloadSelected): - 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.updateVideoRenameExample() - self.updatePhotoDownloadFolderExample() - self.updateVideoDownloadFolderExample() - - def on_remove_all_job_code_button_clicked(self, button): - j = RemoveAllJobCodeDialog(self.widget, self.remove_all_job_code) - - def remove_all_job_code(self, dialog, userSelected): - dialog.destroy() - if userSelected: - self.job_code_liststore.clear() - self.update_job_codes() - self.updateImageRenameExample() - self.updateVideoRenameExample() - self.updatePhotoDownloadFolderExample() - self.updateVideoDownloadFolderExample() - - 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.updateVideoRenameExample() - self.updatePhotoDownloadFolderExample() - self.updateVideoDownloadFolderExample() - - 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_delete_checkbutton_toggled(self, checkbutton): - self.prefs.auto_delete = 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_treeview_cursor_changed(self, tree): - path, column = tree.get_cursor() - self.notebook.set_current_page(path[0]) - - def on_synchronize_raw_jpg_checkbutton_toggled(self, check_button): - self.prefs.synchronize_raw_jpg = check_button.get_active() - - def on_strip_characters_checkbutton_toggled(self, check_button): - self.prefs.strip_characters = check_button.get_active() - self.updateImageRenameExample() - self.updatePhotoDownloadFolderExample() - self.updateVideoDownloadFolderExample() - - 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 + self._backupVideoControls: - 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) - for c in self._backupVideoControls: - c.set_sensitive(False) - if DOWNLOAD_VIDEO: - for c in self._backupVideoControls: - c.set_sensitive(True) - else: - for c in self._backupControls1: - c.set_sensitive(False) - for c in self._backupControls2: - c.set_sensitive(True) - if DOWNLOAD_VIDEO: - for c in self._backupVideoControls: - c.set_sensitive(False) - - def disableVideoControls(self): - """ - Disables video preferences if video downloading is disabled - (probably because the appropriate libraries to enable - video metadata extraction are not installed) - """ - controls = [self.example_video_filename_label, - self.original_video_filename_label, - self.new_video_filename_label, - self.video_new_name_label, - self.video_original_name_label, - self.video_rename_scrolledwindow, - self.video_folders_hbox, - self.video_backup_identifier_label, - self.video_backup_identifier_entry - ] - for c in controls: - c.set_sensitive(False) - - self.videos_cannot_be_downloaded_label.show() - self.folder_videos_cannot_be_downloaded_label.show() - self.folder_videos_cannot_be_downloaded_hbox.show() - - 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_video_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. - drive1 = 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. - drive2 = os.path.join(config.MEDIA_LOCATION, _("externaldrive2")) - - path = os.path.join(drive1, self.backup_identifier_entry.get_text()) - path2 = os.path.join(drive2, self.backup_identifier_entry.get_text()) - path3 = os.path.join(drive2, self.video_backup_identifier_entry.get_text()) - path = common.escape(path) - path2 = common.escape(path2) - path3 = common.escape(path3) - if DOWNLOAD_VIDEO: - example = "<i>%s</i>\n<i>%s</i>\n<i>%s</i>" % (path, path2, path3) - else: - example = "<i>%s</i>\n<i>%s</i>" % (path, path2) - self.example_backup_path_label.set_markup(example) +#Translators: if neccessary, for guidance in how to translate this program, you may see http://damonlynch.net/translate.html +PROGRAM_NAME = _('Rapid Photo Downloader') +__version__ = config.version - -def file_types_by_number(noImages, noVideos): - """ - returns a string to be displayed to the user that can be used - to show if a value refers to photos or videos or both, or just one - of each - """ - if (noVideos > 0) and (noImages > 0): - v = _('photos and videos') - elif (noVideos == 0) and (noImages == 0): - v = _('photos or videos') - elif noVideos > 0: - if noVideos > 1: - v = _('videos') - else: - v = _('video') - else: - if noImages > 1: - v = _('photos') - else: - v = _('photo') - return v - def date_time_human_readable(date, with_line_break=True): if with_line_break: return _("%(date)s\n%(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")} else: return _("%(date)s %(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")} -def time_subseconds_human_readable(date, subseconds): - return _("%(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \ - {'hour':date.strftime("%H"), - 'minute':date.strftime("%M"), - 'second':date.strftime("%S"), - 'subsecond': subseconds} - def date_time_subseconds_human_readable(date, subseconds): return _("%(date)s %(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \ {'date':date.strftime("%x"), @@ -1588,1498 +119,22 @@ def date_time_subseconds_human_readable(date, subseconds): 'second':date.strftime("%S"), 'subsecond': subseconds} -def generateSubfolderAndName(mediaFile, problem, subfolderPrefsFactory, - renamePrefsFactory, - nameUsesJobCode, subfolderUsesJobCode, - strip_characters, fallback_date): - - subfolderPrefsFactory.initializeProblem(problem) - mediaFile.sampleSubfolder = subfolderPrefsFactory.generateNameUsingPreferences( - mediaFile.metadata, mediaFile.name, - strip_characters, - fallback_date = fallback_date) - - mediaFile.samplePath = os.path.join(mediaFile.downloadFolder, mediaFile.sampleSubfolder) - - renamePrefsFactory.initializeProblem(problem) - mediaFile.sampleName = renamePrefsFactory.generateNameUsingPreferences( - mediaFile.metadata, mediaFile.name, strip_characters, - sequencesPreliminary=False, - fallback_date = fallback_date) - - if not (mediaFile.sampleName or nameUsesJobCode) or not (mediaFile.sampleSubfolder or subfolderUsesJobCode): - if not (mediaFile.sampleName or nameUsesJobCode) and not (mediaFile.sampleSubfolder or subfolderUsesJobCode): - area = _("subfolder and filename") - elif not (mediaFile.sampleName or nameUsesJobCode): - area = _("filename") - else: - area = _("subfolder") - problem.add_problem(None, pn.ERROR_IN_NAME_GENERATION, {'filetype': mediaFile.displayNameCap, 'area': area}) - problem.add_extra_detail(pn.NO_DATA_TO_NAME, {'filetype': area}) - mediaFile.problem = problem - mediaFile.status = STATUS_CANNOT_DOWNLOAD - elif problem.has_problem(): - mediaFile.problem = problem - mediaFile.status = STATUS_WARNING - else: - mediaFile.status = STATUS_NOT_DOWNLOADED - -def getGenericPhotoImage(): - return gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/photo.png')) - -def getGenericVideoImage(): - return gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/video.png')) -class NeedAJobCode(): +class DeviceCollection(gtk.TreeView): """ - Convenience class to check whether a job code is missing for a given - file type (photo or video) + TreeView display of devices and how many files have been copied, shown + immediately under the menu in the main application window. """ - def __init__(self, prefs): - self.imageRenameUsesJobCode = rn.usesJobCode(prefs.image_rename) - self.imageSubfolderUsesJobCode = rn.usesJobCode(prefs.subfolder) - self.videoRenameUsesJobCode = rn.usesJobCode(prefs.video_rename) - self.videoSubfolderUsesJobCode = rn.usesJobCode(prefs.video_subfolder) - - def needAJobCode(self, job_code, is_image): - if is_image: - return not job_code and (self.imageRenameUsesJobCode or self.imageSubfolderUsesJobCode) - else: - return not job_code and (self.videoRenameUsesJobCode or self.videoSubfolderUsesJobCode) - - -class CopyPhotos(Thread): - """Copies photos from source to destination, backing up if needed""" - def __init__(self, thread_id, parentApp, fileRenameLock, fileSequenceLock, - statsLock, downloadedFilesLock, - 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.downloadedFilesLock = downloadedFilesLock - - 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.scanComplete = self.downloadStarted = self.downloadComplete = False - - # Need to account for situations where the user adjusts their preferences when the program is scanning - # Here the sample filenames and paths will be out of date, and they will need to be updated - # This flag indicates whether that is the case or not - self.scanResultsStale = False # name and subfolder - self.scanResultsStaleDownloadFolder = False #download folder only - - self.noErrors = self.noWarnings = 0 - self.videoTempWorkingDir = self.photoTempWorkingDir = '' - - if DOWNLOAD_VIDEO: - self.types_searched_for = _('photos or videos') - else: - self.types_searched_for = _('photos') - - Thread.__init__(self) - - - def initializeDisplay(self, thread_id, cardMedia = None): + def __init__(self, parent_app): - if self.cardMedia: - media_collection_treeview.addCard(thread_id, self.cardMedia.prettyName(), - '', progress=0.0, - # This refers to when a device like a hard drive is having its contents scanned, - # looking for photos or videos. It is visible initially in the progress bar for each device - # (which normally holds "x photos and videos"). - # It maybe displayed only briefly if the contents of the device being scanned is small. - progressBarText=_('scanning...')) - - def firstImage(self): - """ - returns class mediaFile of the first photo - """ - mediaFile = self.cardMedia.firstImage() - return mediaFile - - def firstVideo(self): - """ - returns class mediaFile of the first video - """ - mediaFile = self.cardMedia.firstVideo() - return mediaFile - - 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 - """ - - def checkPrefs(prefsFactory): - try: - prefsFactory.checkPrefsForValidity() - except (rn.PrefValueInvalidError, rn.PrefLengthError, - rn.PrefValueKeyComboError, rn.PrefKeyError), e: - if notifyOnError: - self.handlePreferencesError(e, prefsFactory) - raise rn.PrefError - - self.prefs = self.parentApp.prefs - - #Image and Video filename preferences - sample_download_start_time = datetime.datetime.now() - - self.imageRenamePrefsFactory = rn.ImageRenamePreferences(self.prefs.image_rename, self, - self.fileSequenceLock, sequences) - self.imageRenamePrefsFactory.setDownloadStartTime(sample_download_start_time) - checkPrefs(self.imageRenamePrefsFactory) - - self.videoRenamePrefsFactory = rn.VideoRenamePreferences(self.prefs.video_rename, self, - self.fileSequenceLock, sequences) - self.videoRenamePrefsFactory.setDownloadStartTime(sample_download_start_time) - checkPrefs(self.videoRenamePrefsFactory) - - #Image and Video subfolder preferences - - self.subfolderPrefsFactory = rn.SubfolderPreferences(self.prefs.subfolder, self) - self.subfolderPrefsFactory.setDownloadStartTime(sample_download_start_time) - checkPrefs(self.subfolderPrefsFactory) - - self.videoSubfolderPrefsFactory = rn.VideoSubfolderPreferences(self.prefs.video_subfolder, self) - self.videoSubfolderPrefsFactory.setDownloadStartTime(sample_download_start_time) - checkPrefs(self.videoSubfolderPrefsFactory) - - # 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 checkDownloadPath(path): - """ - Checks to see if download folder exists. - - Creates it if it does not exist. - - Returns False if the path could not be created. - """ - - try: - if not os.path.isdir(path): - os.makedirs(path) - return True - - except: - display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) - msg = _("The following download path could not be created:\n") - msg += _("%(path)s: ") % {'path': path} - logError(config.CRITICAL_ERROR, _("Download cannot proceed"), msg) - cmd_line(_("Download cannot proceed")) - cmd_line(msg) - display_queue.put((self.parentApp.downloadFailed, (self.thread_id, ))) - display_queue.close("rw") - return False - - 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) - display_queue.put((self.parentApp.downloadFailed, (self.thread_id, ))) - display_queue.close("rw") - return False - - - - def scanMedia(): - """ - Scans media for photos and videos - """ - - # load images to display for when a thumbnail cannot be extracted or created - - self.photoThumbnail = getGenericPhotoImage() - self.videoThumbnail = getGenericVideoImage() - - imageRenameUsesJobCode = rn.usesJobCode(self.prefs.image_rename) - imageSubfolderUsesJobCode = rn.usesJobCode(self.prefs.subfolder) - videoRenameUsesJobCode = rn.usesJobCode(self.prefs.video_rename) - videoSubfolderUsesJobCode = rn.usesJobCode(self.prefs.video_subfolder) - - def loadFileMetadata(mediaFile): - """ - loads the metadate for the file, and additional information if required - """ - - problem = pn.Problem() - try: - mediaFile.loadMetadata() - except: - mediaFile.status = STATUS_CANNOT_DOWNLOAD - mediaFile.metadata = None - problem.add_problem(None, pn.CANNOT_DOWNLOAD_BAD_METADATA, {'filetype': mediaFile.displayNameCap}) - mediaFile.problem = problem - else: - # generate sample filename and subfolder - if mediaFile.isImage: - fallback_date = None - subfolderPrefsFactory = self.subfolderPrefsFactory - renamePrefsFactory = self.imageRenamePrefsFactory - nameUsesJobCode = imageRenameUsesJobCode - subfolderUsesJobCode = imageSubfolderUsesJobCode - else: - fallback_date = mediaFile.modificationTime - subfolderPrefsFactory = self.videoSubfolderPrefsFactory - renamePrefsFactory = self.videoRenamePrefsFactory - nameUsesJobCode = videoRenameUsesJobCode - subfolderUsesJobCode = videoSubfolderUsesJobCode - - generateSubfolderAndName(mediaFile, problem, subfolderPrefsFactory, renamePrefsFactory, - nameUsesJobCode, subfolderUsesJobCode, - self.prefs.strip_characters, fallback_date) - # generate thumbnail - mediaFile.generateThumbnail(self.videoTempWorkingDir) - - if mediaFile.thumbnail is None: - addGenericThumbnail(mediaFile) - - - def addGenericThumbnail(mediaFile): - """ - Adds a generic thumbnail to the mediafile, which - can be very useful when previews are disabled - """ - mediaFile.genericThumbnail = True - if mediaFile.isImage: - mediaFile.thumbnail = self.photoThumbnail - else: - mediaFile.thumbnail = self.videoThumbnail - - def downloadable(name): - isImage = media.isImage(name) - isVideo = media.isVideo(name) - download = (DOWNLOAD_VIDEO and (isImage or isVideo) or - ((not DOWNLOAD_VIDEO) and isImage)) - return (download, isImage, isVideo) - - def addFile(name, path, size, modificationTime, device, volume, isImage): - """ - Add an image or video to the list of scanned files to be shown to the user for potential downloading - """ - - if isImage: - downloadFolder = self.prefs.download_folder - else: - downloadFolder = self.prefs.video_download_folder - - mediaFile = media.MediaFile(self.thread_id, name, path, size, modificationTime, device, downloadFolder, volume, isImage) - loadFileMetadata(mediaFile) - # modificationTime is very useful for quick sorting - imagesAndVideos.append((mediaFile, modificationTime)) - display_queue.put((self.parentApp.addFile, (mediaFile,))) - - if isImage: - self.noImages += 1 - else: - self.noVideos += 1 - - - def gio_scan(path, fileSizeSum): - """recursive function to scan a directory and its subdirectories - for photos and possibly videos""" - - children = path.enumerate_children('standard::name,standard::type,standard::size,time::modified') - - for child in children: - if not self.running: - self.lock.acquire() - self.running = True - - if not self.ctrl: - return None - - if child.get_file_type() == gio.FILE_TYPE_DIRECTORY: - fileSizeSum = gio_scan(path.get_child(child.get_name()), fileSizeSum) - if fileSizeSum == None: - # this value will be None only if the thread is exiting - return None - elif child.get_file_type() == gio.FILE_TYPE_REGULAR: - name = child.get_name() - download, isImage, isVideo = downloadable(name) - if download: - size = child.get_size() - modificationTime = child.get_modification_time() - addFile(name, path.get_path(), size, modificationTime, self.cardMedia.prettyName(limit=0), self.cardMedia.volume, isImage) - fileSizeSum += size - - return fileSizeSum - - - imagesAndVideos = [] - fileSizeSum = 0 - self.noVideos = 0 - self.noImages = 0 - - if not using_gio or not self.cardMedia.volume: - 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: - return None - - - download, isImage, isVideo = downloadable(name) - if download: - fullFileName = os.path.join(root, name) - size = os.path.getsize(fullFileName) - modificationTime = os.path.getmtime(fullFileName) - addFile(name, root, size, modificationTime, self.cardMedia.prettyName(limit=0), self.cardMedia.volume, isImage) - fileSizeSum += size - - - else: - # using gio and have a volume - # make call to recursive function to scan volume - fileSizeSum = gio_scan(self.cardMedia.volume.volume.get_root(), fileSizeSum) - if fileSizeSum == None: - # thread exiting - return None - - # sort in place based on modification time - imagesAndVideos.sort(key=operator.itemgetter(1)) - noFiles = len(imagesAndVideos) - - self.scanComplete = True - - self.display_file_types = file_types_by_number(self.noImages, self.noVideos) - - - if noFiles: - self.cardMedia.setMedia(imagesAndVideos, fileSizeSum, noFiles) - # 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 photos that can be copied. For example, the user might see the following: - # '0 of 512 photos' or '0 of 10 videos' or '0 of 202 photos and videos'. - # This particular text is displayed to the user before the download has started. - display = _("%(number)s %(filetypes)s") % {'number':noFiles, 'filetypes':self.display_file_types} - display_queue.put((media_collection_treeview.updateCard, (self.thread_id, self.cardMedia.sizeOfImagesAndVideos()))) - display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, 0.0, display, 0))) - 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 photos 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 %(filetypes)s on %(device)s") % - {'number': noFiles, 'filetypes':self.display_file_types, - '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 %(filetypes)s found on %(device)s") % {'device':self.cardMedia.prettyName(limit=0), 'filetypes':self.types_searched_for}) - return False - - - 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 notifyAndUnmount(umountAttemptOK): - if not self.cardMedia.volume: - unmountMessage = "" - notificationName = PROGRAM_NAME - else: - notificationName = self.cardMedia.volume.get_name() - if self.prefs.auto_unmount and umountAttemptOK: - 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 = "" - - file_types = file_types_by_number(noImagesDownloaded, noVideosDownloaded) - file_types_skipped = file_types_by_number(noImagesSkipped, noVideosSkipped) - message = _("%(noFiles)s %(filetypes)s downloaded") % {'noFiles':noFilesDownloaded, 'filetypes': file_types} - noFilesSkipped = noImagesSkipped + noVideosSkipped - if noFilesSkipped: - message += "\n" + _("%(noFiles)s %(filetypes)s failed to download") % {'noFiles':noFilesSkipped, 'filetypes':file_types_skipped} - - if self.noWarnings: - message = "%s\n%s " % (message, self.noWarnings) + _("warnings") - if self.noErrors: - message = "%s\n%s " % (message, self.noErrors) + _("errors") - - if unmountMessage: - message = "%s\n%s" % (message, unmountMessage) - - n = pynotify.Notification(notificationName, message) - - if self.cardMedia.volume: - icon = self.cardMedia.volume.get_icon_pixbuf(self.parentApp.notification_icon_size) - else: - icon = self.parentApp.application_icon - - n.set_icon_from_pixbuf(icon) - n.show() - - def createTempDir(baseDir): - """ - Create a temporary directory in which to download the photos to. - - Returns the directory if it was created, else returns None. - - 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 actual download folder will be slow!""" - try: - t = tempfile.mkdtemp(prefix='rapid-tmp-', - dir=baseDir) - return t - except OSError, (errno, strerror): - if not self.cardMedia.volume: - image_device = _("Source: %s\n") % self.cardMedia.getPath() - else: - _("Device: %s\n") % self.cardMedia.volume.get_name() - destination = _("Destination: %s") % baseDir - logError(config.CRITICAL_ERROR, _('Could not create temporary download directory'), - image_device + destination, - _("Download cannot proceed")) - cmd_line(_("Error:") + " " + _('Could not create temporary download directory')) - cmd_line(image_device + destination) - cmd_line(_("Download cannot proceed")) - display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) - display_queue.put((self.parentApp.downloadFailed, (self.thread_id, ))) - display_queue.close("rw") - self.running = False - self.lock.release() - return None - - def setupBackup(): - """ - Check for presence of backup path or volumes, and return the number of devices being used (1 in case of a path) - """ - no_devices = 0 - if self.prefs.backup_images: - no_devices = len(self.parentApp.backupVolumes) - if not self.prefs.backup_device_autodetection: - if not os.path.isdir(self.prefs.backup_location): - # the user has manually specified a path, but it - # does not exist. This is a problem. - try: - os.makedirs(self.prefs.backup_location) - except: - logError(config.SERIOUS_ERROR, _("Backup path does not exist"), - _("The path %s could not be created") % path, - _("No backups can occur") - ) - no_devices = 0 - return no_devices - - def checkIfNeedAJobCode(): - needAJobCode = NeedAJobCode(self.prefs) - - for f in self.cardMedia.imagesAndVideos: - mediaFile = f[0] - if mediaFile.status in [STATUS_WARNING, STATUS_NOT_DOWNLOADED]: - if needAJobCode.needAJobCode(mediaFile.jobcode, mediaFile.isImage): - return True - return False - - def createBothTempDirs(): - self.photoTempWorkingDir = createTempDir(photoBaseDownloadDir) - created = self.photoTempWorkingDir is not None - if created and DOWNLOAD_VIDEO: - self.videoTempWorkingDir = createTempDir(videoBaseDownloadDir) - created = self.videoTempWorkingDir is not None - - return created - - - def checkProblemWithNameGeneration(mediaFile): - if mediaFile.problem.has_problem(): - logError(config.WARNING, - mediaFile.problem.get_title(), - _("Source: %(source)s\nDestination: %(destination)s\n%(problem)s") % - {'source': mediaFile.fullFileName, 'destination': mediaFile.downloadFullFileName, 'problem': mediaFile.problem.get_problems()}) - mediaFile.status = STATUS_DOWNLOADED_WITH_WARNING - - def fileAlreadyExists(mediaFile, identifier=None): - """ Notify the user that the photo or video could not be downloaded because it already exists""" - - # get information on when the existing file was last modified - try: - modificationTime = os.path.getmtime(mediaFile.downloadFullFileName) - dt = datetime.datetime.fromtimestamp(modificationTime) - date = dt.strftime("%x") - time = dt.strftime("%X") - except: - sys.stderr.write("WARNING: could not determine the file modification time of an existing file\n") - date = time = '' - - if not identifier: - mediaFile.problem.add_problem(None, pn.FILE_ALREADY_EXISTS_NO_DOWNLOAD, {'filetype':mediaFile.displayNameCap}) - mediaFile.problem.add_extra_detail(pn.EXISTING_FILE, {'filetype': mediaFile.displayName, 'date': date, 'time': time}) - mediaFile.status = STATUS_DOWNLOAD_FAILED - log_status = config.SERIOUS_ERROR - problem_text = pn.extra_detail_definitions[pn.EXISTING_FILE] % {'date':date, 'time':time, 'filetype': mediaFile.displayName} - else: - mediaFile.problem.add_problem(None, pn.UNIQUE_IDENTIFIER_ADDED, {'filetype':mediaFile.displayNameCap}) - mediaFile.problem.add_extra_detail(pn.UNIQUE_IDENTIFIER, {'identifier': identifier, 'filetype': mediaFile.displayName, 'date': date, 'time': time}) - mediaFile.status = STATUS_DOWNLOADED_WITH_WARNING - log_status = config.WARNING - problem_text = pn.extra_detail_definitions[pn.UNIQUE_IDENTIFIER] % {'identifier': identifier, 'filetype': mediaFile.displayName, 'date': date, 'time': time} - - logError(log_status, mediaFile.problem.get_title(), - _("Source: %(source)s\nDestination: %(destination)s") - % {'source': mediaFile.fullFileName, 'destination': mediaFile.downloadFullFileName}, - problem_text) - - def downloadCopyingError(mediaFile, inst=None, errno=None, strerror=None): - """Notify the user that an error occurred (most likely at the OS / filesystem level) when coyping a photo or video""" - - if errno != None and strerror != None: - mediaFile.problem.add_problem(None, pn.DOWNLOAD_COPYING_ERROR_W_NO, {'filetype': mediaFile.displayName}) - mediaFile.problem.add_extra_detail(pn.DOWNLOAD_COPYING_ERROR_W_NO_DETAIL, {'errorno': errno, 'strerror': strerror}) - - else: - mediaFile.problem.add_problem(None, pn.DOWNLOAD_COPYING_ERROR, {'filetype': mediaFile.displayName}) - if not inst: - # hopefully inst will never be None, but just to be safe... - inst = _("Please check your system and try again.") - mediaFile.problem.add_extra_detail(pn.DOWNLOAD_COPYING_ERROR_DETAIL, inst) - - logError(config.SERIOUS_ERROR, mediaFile.problem.get_title(), mediaFile.problem.get_problems()) - mediaFile.status = STATUS_DOWNLOAD_FAILED - - def sameNameDifferentExif(image_name, mediaFile): - """Notify the user that a file was already downloaded with the same name, but the exif information was different""" - i1_ext, i1_date_time, i1_subseconds = downloaded_files.extExifDateTime(image_name) - detail = {'image1': "%s%s" % (image_name, i1_ext), - 'image1_date': i1_date_time.strftime("%x"), - 'image1_time': time_subseconds_human_readable(i1_date_time, i1_subseconds), - 'image2': mediaFile.name, - 'image2_date': mediaFile.metadata.dateTime().strftime("%x"), - 'image2_time': time_subseconds_human_readable( - mediaFile.metadata.dateTime(), - mediaFile.metadata.subSeconds())} - mediaFile.problem.add_problem(None, pn.SAME_FILE_DIFFERENT_EXIF, detail) - - msg = pn.problem_definitions[pn.SAME_FILE_DIFFERENT_EXIF][1] % detail - logError(config.WARNING,_('Photos detected with the same filenames, but taken at different times'), msg) - mediaFile.status = STATUS_DOWNLOADED_WITH_WARNING - - def generateSubfolderAndFileName(mediaFile): - """ - Generates subfolder and file names for photos and videos - """ - - skipFile = alreadyDownloaded = False - sequence_to_use = None - - if mediaFile.isVideo: - fileRenameFactory = self.videoRenamePrefsFactory - subfolderFactory = self.videoSubfolderPrefsFactory - else: - # file is an photo - fileRenameFactory = self.imageRenamePrefsFactory - subfolderFactory = self.subfolderPrefsFactory - - fileRenameFactory.setJobCode(mediaFile.jobcode) - subfolderFactory.setJobCode(mediaFile.jobcode) - - mediaFile.problem = pn.Problem() - subfolderFactory.initializeProblem(mediaFile.problem) - fileRenameFactory.initializeProblem(mediaFile.problem) - - # Here we cannot assume that the subfolder value will contain something -- the user may have changed the preferences after the scan - mediaFile.downloadSubfolder = subfolderFactory.generateNameUsingPreferences( - mediaFile.metadata, mediaFile.name, - self.stripCharacters, fallback_date = mediaFile.modificationTime) - - - if self.prefs.synchronize_raw_jpg and usesImageSequenceElements and mediaFile.isImage: - #synchronizing RAW and JPEG only applies to photos, not videos - image_name, image_ext = os.path.splitext(mediaFile.name) - with self.downloadedFilesLock: - i, sequence_to_use = downloaded_files.matching_pair(image_name, image_ext, mediaFile.metadata.dateTime(), mediaFile.metadata.subSeconds()) - if i == -1: - # this exact file has already been downloaded (same extension, same filename, and same exif date time subsecond info) - if not addUniqueIdentifier: - logError(config.SERIOUS_ERROR,_('Photo has already been downloaded'), - _("Source: %(source)s") % {'source': mediaFile.fullFileName}) - mediaFile.problem.add_problem(None, pn.FILE_ALREADY_DOWNLOADED, {'filetype': mediaFile.displayNameCap}) - skipFile = True - - - # pass the subfolder the image will go into, as this is needed to determine subfolder sequence numbers - # indicate that sequences chosen should be queued - - if not skipFile: - mediaFile.downloadName = fileRenameFactory.generateNameUsingPreferences( - mediaFile.metadata, mediaFile.name, self.stripCharacters, mediaFile.downloadSubfolder, - sequencesPreliminary = True, - sequence_to_use = sequence_to_use, - fallback_date = mediaFile.modificationTime) - - mediaFile.downloadPath = os.path.join(mediaFile.downloadFolder, mediaFile.downloadSubfolder) - mediaFile.downloadFullFileName = os.path.join(mediaFile.downloadPath, mediaFile.downloadName) - - if not mediaFile.downloadName or not mediaFile.downloadSubfolder: - if not mediaFile.downloadName and not mediaFile.downloadSubfolder: - area = _("subfolder and filename") - elif not mediaFile.downloadName: - area = _("filename") - else: - area = _("subfolder") - problem.add_problem(None, pn.ERROR_IN_NAME_GENERATION, {'filetype': mediaFile.displayNameCap, 'area': area}) - problem.add_extra_detail(pn.NO_DATA_TO_NAME, {'filetype': area}) - skipFile = True - logError(config.SERIOUS_ERROR, pn.problem_definitions[ERROR_IN_NAME_GENERATION][1] % {'filetype': mediaFile.displayNameCap, 'area': area}) - - if not skipFile: - checkProblemWithNameGeneration(mediaFile) - else: - self.sizeDownloaded += mediaFile.size * (no_backup_devices + 1) - mediaFile.status = STATUS_DOWNLOAD_FAILED - - return (skipFile, sequence_to_use) - - def progress_callback(amount_downloaded, total): - if (amount_downloaded - self.bytes_downloaded > 2097152) or (amount_downloaded == total): - chunk_downloaded = amount_downloaded - self.bytes_downloaded - self.bytes_downloaded = amount_downloaded - percentComplete = (float(self.sizeDownloaded + amount_downloaded) / sizeFiles) * 100 - - display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, percentComplete, None, chunk_downloaded))) - - def downloadFile(mediaFile, sequence_to_use): - """ - Downloads the photo or video file to the specified subfolder - """ - - if not mediaFile.isImage: - renameFactory = self.videoRenamePrefsFactory - else: - renameFactory = self.imageRenamePrefsFactory - - def progress_callback_no_update(amount_downloaded, total): - pass - - try: - fileDownloaded = False - if not os.path.isdir(mediaFile.downloadPath): - os.makedirs(mediaFile.downloadPath) - - nameUniqueBeforeCopy = True - downloadNonUniqueFile = True - - # do a preliminary check to see if a file with the same name already exists - if os.path.exists(mediaFile.downloadFullFileName): - nameUniqueBeforeCopy = False - if not addUniqueIdentifier: - downloadNonUniqueFile = False - if (usesVideoSequenceElements and not mediaFile.isImage) or (usesImageSequenceElements and mediaFile.isImage and not self.prefs.synchronize_raw_jpg): - # potentially, a unique file name could still be generated - # investigate this possibility - with self.fileSequenceLock: - for possibleName in renameFactory.generateNameSequencePossibilities( - mediaFile.metadata, - mediaFile.name, self.stripCharacters, mediaFile.downloadSubfolder, - fallback_date = mediaFile.modificationTime): - if possibleName: - # no need to check for any problems here, it's just a temporary name - possibleFile = os.path.join(mediaFile.downloadPath, possibleName) - possibleTempFile = os.path.join(tempWorkingDir, possibleName) - if not os.path.exists(possibleFile) and not os.path.exists(possibleTempFile): - downloadNonUniqueFile = True - break - - - if not downloadNonUniqueFile: - fileAlreadyExists(mediaFile) - - copy_succeeded = False - if nameUniqueBeforeCopy or downloadNonUniqueFile: - tempWorkingfile = os.path.join(tempWorkingDir, mediaFile.downloadName) - if using_gio: - g_dest = gio.File(path=tempWorkingfile) - g_src = gio.File(path=mediaFile.fullFileName) - try: - if not g_src.copy(g_dest, progress_callback, cancellable=gio.Cancellable()): - downloadCopyingError(mediaFile) - else: - copy_succeeded = True - except glib.GError, inst: - downloadCopyingError(mediaFile, inst=inst) - else: - shutil.copy2(mediaFile.fullFileName, tempWorkingfile) - copy_succeeded = True - - if copy_succeeded: - with self.fileRenameLock: - doRename = True - if usesSequenceElements: - with self.fileSequenceLock: - # get a filename and use this as the "real" filename - if sequence_to_use is None and self.prefs.synchronize_raw_jpg and mediaFile.isImage: - # must check again, just in case the matching pair has been downloaded in the meantime - image_name, image_ext = os.path.splitext(mediaFile.name) - with self.downloadedFilesLock: - i, sequence_to_use = downloaded_files.matching_pair(image_name, image_ext, mediaFile.metadata.dateTime(), mediaFile.metadata.subSeconds()) - if i == -99: - sameNameDifferentExif(image_name, mediaFile) - - mediaFile.downloadName = renameFactory.generateNameUsingPreferences( - mediaFile.metadata, mediaFile.name, self.stripCharacters, mediaFile.downloadSubfolder, - sequencesPreliminary = False, - sequence_to_use = sequence_to_use, - fallback_date = mediaFile.modificationTime) - - if not mediaFile.downloadName: - # there was a serious error generating the filename - doRename = False - else: - mediaFile.downloadFullFileName = os.path.join(mediaFile.downloadPath, mediaFile.downloadName) - # check if the file exists again - if os.path.exists(mediaFile.downloadFullFileName): - if not addUniqueIdentifier: - doRename = False - fileAlreadyExists(mediaFile) - else: - # add basic suffix to make the filename unique - name = os.path.splitext(mediaFile.downloadName) - suffixAlreadyUsed = True - while suffixAlreadyUsed: - if mediaFile.downloadFullFileName in duplicate_files: - duplicate_files[mediaFile.downloadFullFileName] += 1 - else: - duplicate_files[mediaFile.downloadFullFileName] = 1 - identifier = '_%s' % duplicate_files[mediaFile.downloadFullFileName] - mediaFile.downloadName = name[0] + identifier + name[1] - possibleNewFile = os.path.join(mediaFile.downloadPath, mediaFile.downloadName) - suffixAlreadyUsed = os.path.exists(possibleNewFile) - - fileAlreadyExists(mediaFile, identifier) - mediaFile.downloadFullFileName = possibleNewFile - - - if doRename: - rename_succeeded = False - if using_gio: - g_dest = gio.File(path=mediaFile.downloadFullFileName) - g_src = gio.File(path=tempWorkingfile) - try: - if not g_src.move(g_dest, progress_callback_no_update, cancellable=gio.Cancellable()): - downloadCopyingError(mediaFile) - else: - rename_succeeded = True - except glib.GError, inst: - downloadCopyingError(mediaFile, inst=inst) - else: - os.rename(tempWorkingfile, mediaFile.downloadFullFileName) - rename_succeeded = True - - if rename_succeeded: - fileDownloaded = True - if mediaFile.status != STATUS_DOWNLOADED_WITH_WARNING: - mediaFile.status = STATUS_DOWNLOADED - if usesImageSequenceElements: - if self.prefs.synchronize_raw_jpg and mediaFile.isImage: - name, ext = os.path.splitext(mediaFile.name) - if sequence_to_use is None: - with self.fileSequenceLock: - seq = renameFactory.sequences.getFinalSequence() - else: - seq = sequence_to_use - with self.downloadedFilesLock: - downloaded_files.add_download(name, ext, mediaFile.metadata.dateTime(), mediaFile.metadata.subSeconds(), seq) - - - with self.fileSequenceLock: - if sequence_to_use is None: - renameFactory.sequences.imageCopySucceeded() - if usesStoredSequenceNo: - self.prefs.stored_sequence_no += 1 - - with self.fileSequenceLock: - if sequence_to_use is None: - 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, OSError), (errno, strerror): - downloadCopyingError(mediaFile, errno=errno, strerror=strerror) - - if usesSequenceElements: - if not fileDownloaded and sequence_to_use is None: - renameFactory.sequences.imageCopyFailed() - - #update record keeping using in tracking progress - self.sizeDownloaded += mediaFile.size - self.bytes_downloaded_in_download = self.bytes_downloaded - - return fileDownloaded - - - def backupFile(mediaFile, fileDownloaded, no_backup_devices): - """ - Backup photo or video to path(s) chosen by the user - - there are three scenarios: - (1) file has just been downloaded and should now be backed up - (2) file was already downloaded on some previous occassion and should still be backed up, because it hasn't been yet - (3) file has been backed up already (or at least, a file with the same name already exists) - - A backup medium can be used to backup photos or videos, or both. - """ - - backed_up = False - fileNotBackedUpMessageDisplayed = False - error_encountered = False - expected_bytes_downloaded = self.sizeDownloaded + no_backup_devices * mediaFile.size - - if no_backup_devices: - for rootBackupDir in self.parentApp.backupVolumes: - self.bytes_downloaded = 0 - if self.prefs.backup_device_autodetection: - volume = self.parentApp.backupVolumes[rootBackupDir].get_name() - if mediaFile.isImage: - backupDir = os.path.join(rootBackupDir, self.prefs.backup_identifier) - else: - backupDir = os.path.join(rootBackupDir, self.prefs.video_backup_identifier) - else: - # photos and videos will be backed up into the same root folder, which the user has manually specified - backupDir = rootBackupDir - volume = backupDir # os.path.split(backupDir)[1] - - # if user has chosen auto detection, then: - # photos should only be backed up to photo backup locations - # videos should only be backed up to video backup locations - # if user did not choose autodetection, and the backup path doesn't exist, then - # will try to create it - if os.path.isdir(backupDir) or not self.prefs.backup_device_autodetection: - - backupPath = os.path.join(backupDir, mediaFile.downloadSubfolder) - newBackupFile = os.path.join(backupPath, mediaFile.downloadName) - copyBackup = True - if os.path.exists(newBackupFile): - # this check is of course not thread safe -- it doesn't need to be, because at this stage the file names are going to be unique - # (the folder structure is the same as the actual download folders, and the file names are unique in them) - copyBackup = self.prefs.backup_duplicate_overwrite - - if copyBackup: - mediaFile.problem.add_problem(None, pn.BACKUP_EXISTS_OVERWRITTEN, volume) - else: - mediaFile.problem.add_problem(None, pn.BACKUP_EXISTS, volume) - severity = config.SERIOUS_ERROR - fileNotBackedUpMessageDisplayed = True - - title = _("Backup of %(file_type)s already exists") % {'file_type': mediaFile.displayName} - details = _("Source: %(source)s\nDestination: %(destination)s") \ - % {'source': mediaFile.fullFileName, 'destination': newBackupFile} - if copyBackup: - resolution = _("Backup %(file_type)s overwritten") % {'file_type': mediaFile.displayName} - else: - if self.prefs.backup_device_autodetection: - volume = self.parentApp.backupVolumes[rootBackupDir].get_name() - resolution = _("%(file_type)s not backed up to %(volume)s") % {'file_type': mediaFile.displayNameCap, 'volume': volume} - else: - resolution = _("%(file_type)s not backed up") % {'file_type': mediaFile.displayNameCap} - logError(severity, title, details, resolution) - - if copyBackup: - if fileDownloaded: - fileToCopy = mediaFile.downloadFullFileName - else: - fileToCopy = mediaFile.fullFileName - if os.path.isdir(backupPath): - pathExists = True - else: - pathExists = False - # create the backup subfolders - if using_gio: - dirs = gio.File(backupPath) - try: - if dirs.make_directory_with_parents(cancellable=gio.Cancellable()): - pathExists = True - except glib.GError, inst: - fileNotBackedUpMessageDisplayed = True - mediaFile.problem.add_problem(None, pn.BACKUP_DIRECTORY_CREATION, volume) - mediaFile.problem.add_extra_detail('%s%s' % (pn.BACKUP_DIRECTORY_CREATION, volume), inst) - error_encountered = True - logError(config.SERIOUS_ERROR, _('Backing up error'), - _("Destination directory could not be created: %(directory)s\n") % - {'directory': backupPath, } + - _("Source: %(source)s\nDestination: %(destination)s") % - {'source': mediaFile.fullFileName, 'destination': newBackupFile} + "\n" + - _("Error: %(inst)s") % {'inst': inst}, - _('The %(file_type)s was not backed up.') % {'file_type': mediaFile.displayName} - ) - else: - # recreate folder structure in backup location - # cannot do os.makedirs(backupPath) - it can give bad results when using external drives - # we know backupDir exists - # all the components of subfolder may not - folders = mediaFile.downloadSubfolder.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) - pathExists = True - except (IOError, OSError), (errno, strerror): - fileNotBackedUpMessageDisplayed = True - inst = "%s: %s" % (errno, strerror) - mediaFile.problem.add_problem(None, pn.BACKUP_DIRECTORY_CREATION, volume) - mediaFile.problem.add_extra_detail('%s%s' % (pn.BACKUP_DIRECTORY_CREATION, volume), inst) - error_encountered = True - logError(config.SERIOUS_ERROR, _('Backing up error'), - _("Destination directory could not be created: %(directory)s\n") % - {'directory': backupPath, } + - _("Source: %(source)s\nDestination: %(destination)s") % - {'source': mediaFile.fullFileName, 'destination': newBackupFile} + "\n" + - _("Error: %(errno)s %(strerror)s") % {'errno': errno, 'strerror': strerror}, - _('The %(file_type)s was not backed up.') % {'file_type': mediaFile.displayName} - ) - - break - - if pathExists: - if using_gio: - g_dest = gio.File(path=newBackupFile) - g_src = gio.File(path=fileToCopy) - if self.prefs.backup_duplicate_overwrite: - flags = gio.FILE_COPY_OVERWRITE - else: - flags = gio.FILE_COPY_NONE - try: - if not g_src.copy(g_dest, progress_callback, flags, cancellable=gio.Cancellable()): - fileNotBackedUpMessageDisplayed = True - mediaFile.problem.add_problem(None, pn.BACKUP_ERROR, volume) - error_encountered = True - else: - backed_up = True - if mediaFile.status == STATUS_DOWNLOAD_FAILED: - mediaFile.problem.add_problem(None, pn.NO_DOWNLOAD_WAS_BACKED_UP, volume) - except glib.GError, inst: - fileNotBackedUpMessageDisplayed = True - mediaFile.problem.add_problem(None, pn.BACKUP_ERROR, volume) - mediaFile.problem.add_extra_detail('%s%s' % (pn.BACKUP_ERROR, volume), inst) - error_encountered = True - logError(config.SERIOUS_ERROR, _('Backing up error'), - _("Source: %(source)s\nDestination: %(destination)s") % - {'source': fileToCopy, 'destination': newBackupFile} + "\n" + - _("Error: %(inst)s") % {'inst': inst}, - _('The %(file_type)s was not backed up.') % {'file_type': mediaFile.displayName} - ) - else: - try: - shutil.copy2(fileToCopy, newBackupFile) - backed_up = True - if mediaFile.status == STATUS_DOWNLOAD_FAILED: - mediaFile.problem.add_problem(None, pn.NO_DOWNLOAD_WAS_BACKED_UP, volume) - - except (IOError, OSError), (errno, strerror): - fileNotBackedUpMessageDisplayed = True - mediaFile.problem.add_problem(None, pn.BACKUP_ERROR, volume) - inst = "%s: %s" % (errno, strerror) - mediaFile.problem.add_extra_detail('%s%s' % (pn.BACKUP_ERROR, volume), inst) - error_encountered = True - logError(config.SERIOUS_ERROR, _('Backing up error'), - _("Source: %(source)s\nDestination: %(destination)s") % - {'source': fileToCopy, 'destination': newBackupFile} + "\n" + - _("Error: %(errno)s %(strerror)s") % {'errno': errno, 'strerror': strerror}, - _('The %(file_type)s was not backed up.') % {'file_type': mediaFile.displayName} - ) - - #update record keeping using in tracking progress - self.sizeDownloaded += mediaFile.size - self.bytes_downloaded_in_backup += self.bytes_downloaded - - if not backed_up and not fileNotBackedUpMessageDisplayed: - # The file has not been backed up to any medium - mediaFile.problem.add_problem(None, pn.NO_BACKUP_PERFORMED, {'filetype': mediaFile.displayNameCap}) - - severity = config.SERIOUS_ERROR - problem = _("%(file_type)s could not be backed up") % {'file_type': mediaFile.displayName} - details = _("Source: %(source)s") % {'source': mediaFile.fullFileName} - if self.prefs.backup_device_autodetection: - resolution = _("No suitable backup volume was found") - else: - resolution = _("A backup location was not found") - logError(severity, problem, details, resolution) - - if backed_up and mediaFile.status == STATUS_DOWNLOAD_FAILED: - mediaFile.problem.add_extra_detail(pn.BACKUP_OK_TYPE, mediaFile.displayNameCap) - - if not backed_up: - if mediaFile.status == STATUS_DOWNLOAD_FAILED: - mediaFile.status = STATUS_DOWNLOAD_AND_BACKUP_FAILED - else: - mediaFile.status = STATUS_BACKUP_PROBLEM - elif error_encountered: - # it was backed up to at least one volume, but there was an error on another backup volume - if mediaFile.status != STATUS_DOWNLOAD_FAILED: - mediaFile.status = STATUS_BACKUP_PROBLEM - - # Take into account instances where a backup device has been removed part way through a download - # (thereby making self.parentApp.backupVolumes have less items than expected) - if self.sizeDownloaded < expected_bytes_downloaded: - self.sizeDownloaded = expected_bytes_downloaded - return backed_up - - - self.hasStarted = True - display_queue.open('w') - - #Do not try to handle any preference errors here - getPrefs(False) - - #Check photo and video download path, create if necessary - photoBaseDownloadDir = self.prefs.download_folder - if not checkDownloadPath(photoBaseDownloadDir): - return # cleanup already done - - if DOWNLOAD_VIDEO: - videoBaseDownloadDir = self.prefs.video_download_folder - if not checkDownloadPath(videoBaseDownloadDir): - return - else: - videoBaseDownloadDir = self.videoTempWorkingDir = None - - if not createBothTempDirs(): - return - - s = scanMedia() - if s is None: - if not self.ctrl: - self.running = False - display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) - display_queue.close("rw") - return - else: - sys.stderr.write("FIXME: scan returned None, but the thread is not meant to be exiting\n") - if not s: - cmd_line(_("This device has no %(types_searched_for)s to download from.") % {'types_searched_for': self.types_searched_for}) - display_queue.put((self.parentApp.downloadFailed, (self.thread_id, ))) - display_queue.close("rw") - self.running = False - return - - if self.scanResultsStale or self.scanResultsStaleDownloadFolder: - display_queue.put((self.parentApp.regenerateScannedDevices, (self.thread_id, ))) - all_files_downloaded = False - - totalNonErrorFiles = self.cardMedia.numberOfFilesNotCannotDownload() - - if not self.autoStart: - # halt thread, waiting to be restarted so download proceeds - self.cleanUp() - 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 createBothTempDirs(): - return - - else: - if need_job_code_for_renaming: - if checkIfNeedAJobCode(): - if job_code == None: - self.cleanUp() - self.waitingForJobCode = True - display_queue.put((self.parentApp.getJobCode, ())) - self.running = False - self.lock.acquire() - - if not self.ctrl: - # thread is exiting - display_queue.close("rw") - return - - self.running = True - self.waitingForJobCode = False - if not createBothTempDirs(): - return - else: - # User has entered a job code, and it's in the global variable - # Assign it to all those files that do not have one - display_queue.put((self.parentApp.selection_vbox.selection_treeview.apply_job_code, (job_code, False, True, self.thread_id))) - - # auto start could be false if the user hit cancel when prompted for a job code - if self.autoStart: - # set all in this thread to download pending - display_queue.put((self.parentApp.selection_vbox.selection_treeview.set_status_to_download_pending, (False, self.thread_id))) - # wait until all the files have had their status set to download pending, and once that is done, restart - self.running = False - self.lock.acquire() - self.running = True - - # set download started time - display_queue.put((self.parentApp.setDownloadStartTime, ())) - - while not all_files_downloaded: - - # set the download start time to be the time that the user clicked the download button, or if on auto start, the value just set - i = 0 - while self.parentApp.download_start_time is None or i > 2: - time.sleep(0.5) - i += 1 - - if self.parentApp.download_start_time: - start_time = self.parentApp.download_start_time - else: - # in a bizarre corner case situation, with mulitple cards of greatly varying size, - # it's possible the start time was set above and then in the meantime unset (very unlikely, but conceivably it could happen) - # fall back to the current time in this less than satisfactory situation - start_time = datetime.datetime.now() - - self.imageRenamePrefsFactory.setDownloadStartTime(start_time) - self.subfolderPrefsFactory.setDownloadStartTime(start_time) - if DOWNLOAD_VIDEO: - self.videoRenamePrefsFactory.setDownloadStartTime(start_time) - self.videoSubfolderPrefsFactory.setDownloadStartTime(start_time) - - self.noErrors = self.noWarnings = 0 - - 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)) - - - noFiles, sizeFiles, fileIndex = self.cardMedia.sizeAndNumberDownloadPending() - cmd_line(_("Attempting to download %s files") % noFiles) - - no_backup_devices = setupBackup() - - # include the time it takes to copy to the backup volumes - sizeFiles = sizeFiles * (no_backup_devices + 1) - - display_queue.put((self.parentApp.timeRemaining.set, (self.thread_id, sizeFiles))) - - i = 0 - self.sizeDownloaded = noFilesDownloaded = noImagesDownloaded = noVideosDownloaded = noImagesSkipped = noVideosSkipped = 0 - filesDownloadedSuccessfully = [] - self.bytes_downloaded_in_backup = 0 - - display_queue.put((self.parentApp.addToTotalDownloadSize, (sizeFiles, ))) - display_queue.put((self.parentApp.setOverallDownloadMark, ())) - display_queue.put((self.parentApp.postStartDownloadTasks, ())) - - sizeFiles = float(sizeFiles) - - addUniqueIdentifier = self.prefs.download_conflict_resolution == config.ADD_UNIQUE_IDENTIFIER - usesImageSequenceElements = self.imageRenamePrefsFactory.usesSequenceElements() - usesVideoSequenceElements = self.videoRenamePrefsFactory.usesSequenceElements() - usesSequenceElements = usesVideoSequenceElements or usesImageSequenceElements - - usesStoredSequenceNo = (self.imageRenamePrefsFactory.usesTheSequenceElement(rn.STORED_SEQ_NUMBER) or - self.videoRenamePrefsFactory.usesTheSequenceElement(rn.STORED_SEQ_NUMBER)) - sequences.setUseOfSequenceElements( - self.imageRenamePrefsFactory.usesTheSequenceElement(rn.SESSION_SEQ_NUMBER), - self.imageRenamePrefsFactory.usesTheSequenceElement(rn.SEQUENCE_LETTER)) - - # reset the progress bar to update the status of this download attempt - progressBarText = _("%(number)s of %(total)s %(filetypes)s") % {'number': 0, 'total': noFiles, 'filetypes':self.display_file_types} - display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, 0.0, progressBarText, 0))) - - - while i < noFiles: - # if the user pauses the download, then this will be triggered - if not self.running: - self.lock.acquire() - self.running = True - - if not self.ctrl: - self.running = False - self.cleanUp() - display_queue.close("rw") - return - - # get information about the image to deduce image name and path - mediaFile = self.cardMedia.imagesAndVideos[fileIndex[i]][0] - if not mediaFile.status == STATUS_DOWNLOAD_PENDING: - sys.stderr.write("FIXME: Thread %s is trying to download a file that it should not be!!" % self.thread_id) - else: - self.bytes_downloaded_in_download = self.bytes_downloaded_in_backup = self.bytes_downloaded = 0 - if mediaFile.isImage: - tempWorkingDir = self.photoTempWorkingDir - baseDownloadDir = photoBaseDownloadDir - else: - tempWorkingDir = self.videoTempWorkingDir - baseDownloadDir = videoBaseDownloadDir - - skipFile, sequence_to_use = generateSubfolderAndFileName(mediaFile) - - if skipFile: - if mediaFile.isImage: - noImagesSkipped += 1 - else: - noVideosSkipped += 1 - else: - fileDownloaded = downloadFile(mediaFile, sequence_to_use) - - if self.prefs.backup_images: - backed_up = backupFile(mediaFile, fileDownloaded, no_backup_devices) - - if fileDownloaded: - noFilesDownloaded += 1 - if mediaFile.isImage: - noImagesDownloaded += 1 - else: - noVideosDownloaded += 1 - if self.prefs.backup_images and backed_up: - filesDownloadedSuccessfully.append(mediaFile.fullFileName) - elif not self.prefs.backup_images: - filesDownloadedSuccessfully.append(mediaFile.fullFileName) - else: - if mediaFile.isImage: - noImagesSkipped += 1 - else: - noVideosSkipped += 1 - - #update the selction treeview in the main window with the new status of the file - display_queue.put((self.parentApp.update_status_post_download, (mediaFile.treerowref, ))) - - percentComplete = (float(self.sizeDownloaded) / sizeFiles) * 100 - - if self.sizeDownloaded == sizeFiles and (totalNonErrorFiles - noFiles): - progressBarText = _("%(number)s of %(total)s %(filetypes)s (%(remaining)s remaining)") % { - 'number': i + 1, 'total': noFiles, 'filetypes':self.display_file_types, - 'remaining': totalNonErrorFiles - noFiles} - else: - progressBarText = _("%(number)s of %(total)s %(filetypes)s") % {'number': i + 1, 'total': noFiles, 'filetypes':self.display_file_types} - - if using_gio: - # do not want to update the progress bar any more than it has already been updated - size = mediaFile.size * (no_backup_devices + 1) - self.bytes_downloaded_in_download - self.bytes_downloaded_in_backup - else: - size = mediaFile.size * (no_backup_devices + 1) - display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, percentComplete, progressBarText, size))) - - i += 1 - - with self.statsLock: - self.downloadStats.adjust(self.sizeDownloaded, noImagesDownloaded, noVideosDownloaded, noImagesSkipped, noVideosSkipped, self.noWarnings, self.noErrors) - - if self.prefs.auto_delete: - j = 0 - for imageOrVideo in filesDownloadedSuccessfully: - try: - os.unlink(imageOrVideo) - j += 1 - except OSError, (errno, strerror): - logError(config.SERIOUS_ERROR, _("Could not delete photo or video from device"), - _("Photo: %(source)s\nError: %(errno)s %(strerror)s") - % {'source': image, 'errno': errno, 'strerror': strerror}) - except: - logError(config.SERIOUS_ERROR, _("Could not delete photo or video from device"), - _("Photo: %(source)s")) - - cmd_line(_("Deleted %(number)i %(filetypes)s from device") % {'number':j, 'filetypes':self.display_file_types}) - - totalNonErrorFiles = totalNonErrorFiles - noFiles - if totalNonErrorFiles == 0: - all_files_downloaded = True - - # must manually delete these variables, or else the media cannot be unmounted (bug in some versions of pyexiv2 / exiv2) - # for some reason directories on the device remain open with read only access, even after these steps - I don't know why - del self.subfolderPrefsFactory, self.imageRenamePrefsFactory, self.videoSubfolderPrefsFactory, self.videoRenamePrefsFactory - for i in self.cardMedia.imagesAndVideos: - i[0].metadata = None - - notifyAndUnmount(umountAttemptOK = all_files_downloaded) - cmd_line(_("Download complete from %s") % self.cardMedia.prettyName(limit=0)) - display_queue.put((self.parentApp.notifyUserAllDownloadsComplete,())) - display_queue.put((self.parentApp.resetSequences,())) - - if all_files_downloaded: - self.downloadComplete = True - else: - self.cleanUp() - self.downloadStarted = False - 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 createBothTempDirs(): - return - - - display_queue.put((self.parentApp.exitOnDownloadComplete, ())) - display_queue.close("rw") - - self.cleanUp() - - self.running = False - if noFiles: - 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 cleanUp(self): - """ - Deletes temporary files and folders - """ - - for tempWorkingDir in (self.videoTempWorkingDir, self.photoTempWorkingDir): - if tempWorkingDir: - # possibly delete any lingering files - if os.path.isdir(tempWorkingDir): - tf = os.listdir(tempWorkingDir) - if tf: - for f in tf: - os.remove(os.path.join(tempWorkingDir, f)) - os.rmdir(tempWorkingDir) - - 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) - """ - - # cleanup any temporary directories and files - self.cleanUp() - - 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 devices and associated copying progress. - - Assumes a threaded environment. - """ - def __init__(self, parentApp): - - self.parentApp = parentApp - # device name, size of images on the device (human readable), copy progress (%), copy text - self.liststore = gtk.ListStore(str, str, float, str) - self.mapThreadToRow = {} + self.parent_app = parent_app + # device icon & name, size of images on the device (human readable), + # copy progress (%), copy text, eject button (None if irrelevant), + # process id + self.liststore = gtk.ListStore(gtk.gdk.Pixbuf, str, str, float, str, + gtk.gdk.Pixbuf, int) + self.map_process_to_row = {} + self.devices_by_scan_pid = {} gtk.TreeView.__init__(self, self.liststore) @@ -3088,587 +143,430 @@ class MediaTreeView(gtk.TreeView): 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) + + # Device refers to a thing like a camera, memory card in its reader, + # external hard drive, Portable Storage Device, etc. + column0 = gtk.TreeViewColumn(_("Device")) + pixbuf_renderer = gtk.CellRendererPixbuf() + text_renderer = gtk.CellRendererText() + text_renderer.props.ellipsize = pango.ELLIPSIZE_MIDDLE + text_renderer.set_fixed_size(160, -1) + eject_renderer = gtk.CellRendererPixbuf() + column0.pack_start(pixbuf_renderer, expand=False) + column0.pack_start(text_renderer, expand=True) + column0.pack_end(eject_renderer, expand=False) + column0.add_attribute(pixbuf_renderer, 'pixbuf', 0) + column0.add_attribute(text_renderer, 'text', 1) + column0.add_attribute(eject_renderer, 'pixbuf', 5) self.append_column(column0) - # Size refers to the total size of images on the device, typically in MB or GB - column1 = gtk.TreeViewColumn(_("Size"), gtk.CellRendererText(), text=1) + + # Size refers to the total size of images on the device, typically in + # MB or GB + column1 = gtk.TreeViewColumn(_("Size"), gtk.CellRendererText(), text=2) self.append_column(column1) column2 = gtk.TreeViewColumn(_("Download Progress"), - gtk.CellRendererProgress(), value=2, text=3) + gtk.CellRendererProgress(), + value=3, + text=4) self.append_column(column2) self.show_all() - def addCard(self, thread_id, cardName, sizeFiles, progress = 0.0, progressBarText = ''): + icontheme = gtk.icon_theme_get_default() + try: + self.eject_pixbuf = icontheme.load_icon('media-eject', 16, + gtk.ICON_LOOKUP_USE_BUILTIN) + except: + self.eject_pixbuf = gtk.gdk.pixbuf_new_from_file( + paths.share_dir('glade3/media-eject.png')) + + self.add_events(gtk.gdk.BUTTON_PRESS_MASK) + self.connect('button-press-event', self.button_clicked) + + + def add_device(self, process_id, device, progress_bar_text = ''): # add the row, and get a temporary pointer to the row - iter = self.liststore.append((cardName, sizeFiles, progress, progressBarText)) + size_files = '' + progress = 0.0 + + if device.mount is None: + eject = None + else: + eject = self.eject_pixbuf + + self.devices_by_scan_pid[process_id] = device + + iter = self.liststore.append((device.get_icon(), + device.get_name(), + size_files, + progress, + progress_bar_text, + eject, + process_id)) - self._setThreadMap(thread_id, iter) + self._set_process_map(process_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) + # please note, at program startup, self.row_height() will be less than it will be when already running + # e.g. when starting with 3 cards, it could be 18, but when adding 2 cards to the already running program + # (with one card at startup), it could be 21 + # must account for header row at the top + row_height = self.get_background_area(0, self.get_column(0))[3] + 1 + height = (len(self.map_process_to_row) + 1) * row_height + self.parent_app.device_collection_scrolledwindow.set_size_request(-1, height) - def updateCard(self, thread_id, totalSizeFiles): + def update_device(self, process_id, total_size_files): """ Updates the size of the photos and videos on the device, displayed to the user """ - if thread_id in self.mapThreadToRow: - iter = self._getThreadMap(thread_id) - self.liststore.set_value(iter, 1, totalSizeFiles) + if process_id in self.map_process_to_row: + iter = self._get_process_map(process_id) + self.liststore.set_value(iter, 2, total_size_files) else: - sys.stderr.write("FIXME: this card is unknown") + logger.critical("This device is unknown") + + def get_device(self, process_id): + return self.devices_by_scan_pid.get(process_id) - def removeCard(self, thread_id): - if thread_id in self.mapThreadToRow: - iter = self._getThreadMap(thread_id) + def remove_device(self, process_id): + if process_id in self.map_process_to_row: + iter = self._get_process_map(process_id) self.liststore.remove(iter) - del self.mapThreadToRow[thread_id] + del self.map_process_to_row[process_id] + del self.devices_by_scan_pid[process_id] + + def get_all_displayed_processes(self): + """ + returns a list of the processes currently being displayed to the user + """ + return self.map_process_to_row.keys() - def _setThreadMap(self, thread_id, iter): + def _set_process_map(self, process_id, iter): """ convert the temporary iter into a tree reference, which is permanent """ path = self.liststore.get_path(iter) - treerowRef = gtk.TreeRowReference(self.liststore, path) - self.mapThreadToRow[thread_id] = treerowRef + treerowref = gtk.TreeRowReference(self.liststore, path) + self.map_process_to_row[process_id] = treerowref - def _getThreadMap(self, thread_id): + def _get_process_map(self, process_id): """ - return the tree iter for this thread + return the tree iter for this process """ - if thread_id in self.mapThreadToRow: - treerowRef = self.mapThreadToRow[thread_id] - path = treerowRef.get_path() + if process_id in self.map_process_to_row: + treerowref = self.map_process_to_row[process_id] + path = treerowref.get_path() iter = self.liststore.get_iter(path) return iter else: return None - def updateProgress(self, thread_id, percentComplete, progressBarText, bytesDownloaded): + def update_progress(self, scan_pid, percent_complete, progress_bar_text, bytes_downloaded): - iter = self._getThreadMap(thread_id) + iter = self._get_process_map(scan_pid) if iter: - self.liststore.set_value(iter, 2, percentComplete) - if progressBarText: - self.liststore.set_value(iter, 3, progressBarText) - if percentComplete or bytesDownloaded: - self.parentApp.updateOverallProgress(thread_id, bytesDownloaded, percentComplete) - + if percent_complete: + self.liststore.set_value(iter, 3, percent_complete) + if progress_bar_text: + self.liststore.set_value(iter, 4, progress_bar_text) + if percent_complete or bytes_downloaded: + pass + #~ logger.info("Implement update overall progress") - 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] + 1 + def button_clicked(self, widget, event): + """ + Look for left single click on eject button + """ + if event.button == 1: + if len (self.liststore): + x = int(event.x) + y = int(event.y) + path, column, cell_x, cell_y = self.get_path_at_pos(x, y) + if path is not None: + if column == self.get_column(0): + if cell_x >= column.get_width() - self.eject_pixbuf.get_width(): + iter = self.liststore.get_iter(path) + if self.liststore.get_value(iter, 5) is not None: + self.unmount(process_id = self.liststore.get_value(iter, 6)) + + def unmount(self, process_id): + device = self.devices_by_scan_pid[process_id] + if device.mount is not None: + logger.debug("Unmounting device with scan pid %s", process_id) + device.mount.unmount(self.unmount_callback) + + + def unmount_callback(self, mount, result): + name = mount.get_name() + try: + mount.unmount_finish(result) + logger.debug("%s successfully unmounted" % name) + except gio.Error, inst: + logger.error("%s did not unmount: %s", name, inst) + + title = _("%(device)s did not unmount") % {'device': name} + message = '%s' % inst + + n = pynotify.Notification(title, message) + n.set_icon_from_pixbuf(self.parent_app.application_icon) + n.show() + + +def create_cairo_image_surface(pil_image, image_width, image_height): + imgd = pil_image.tostring("raw","BGRA", 0, 1) + data = array.array('B',imgd) + stride = image_width * 4 + image = cairo.ImageSurface.create_for_data(data, cairo.FORMAT_ARGB32, + image_width, image_height, stride) + return image + +class ThumbnailCellRenderer(gtk.CellRenderer): + __gproperties__ = { + "image": (gobject.TYPE_PYOBJECT, "Image", + "Image", gobject.PARAM_READWRITE), + + "filename": (gobject.TYPE_STRING, "Filename", + "Filename", '', gobject.PARAM_READWRITE), + + "status": (gtk.gdk.Pixbuf, "Status", + "Status", gobject.PARAM_READWRITE), + } + + def __init__(self, checkbutton_height): + gtk.CellRenderer.__init__(self) + self.image = None + + self.image_area_size = 100 + self.text_area_size = 30 + self.padding = 6 + self.checkbutton_height = checkbutton_height + self.icon_width = 20 + + def do_set_property(self, pspec, value): + setattr(self, pspec.name, value) -class ShowWarningDialog(gtk.Dialog): - """ - Displays a warning to the user that downloading directly from a - camera does not always work well - """ - def __init__(self, parent_window, postChoiceCB): - gtk.Dialog.__init__(self, _("Downloading From Cameras"), None, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_OK, gtk.RESPONSE_OK)) - - self.postChoiceCB = postChoiceCB + def do_get_property(self, pspec): + return getattr(self, pspec.name) - primary_msg = _("Downloading directly from a camera may work poorly or not at all") - secondary_msg = _("Downloading from a card reader always works and is generally much faster. It is strongly recommended to use a card reader.") + def do_render(self, window, widget, background_area, cell_area, expose_area, flags): - self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) - - primary_label = gtk.Label() - primary_label.set_markup("<b>%s</b>" % primary_msg) - primary_label.set_line_wrap(True) - primary_label.set_alignment(0, 0.5) - - secondary_label = gtk.Label() - secondary_label.set_text(secondary_msg) - secondary_label.set_line_wrap(True) - secondary_label.set_alignment(0, 0.5) - - self.show_again_checkbutton = gtk.CheckButton(_('_Show this message again'), True) - self.show_again_checkbutton.set_active(True) + cairo_context = window.cairo_create() - msg_vbox = gtk.VBox() - msg_vbox.pack_start(primary_label, False, False, padding=6) - msg_vbox.pack_start(secondary_label, False, False, padding=6) - msg_vbox.pack_start(self.show_again_checkbutton) - - icon = parent_window.render_icon(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG) - image = gtk.Image() - image.set_from_pixbuf(icon) - image.set_alignment(0, 0) - - warning_hbox = gtk.HBox() - warning_hbox.pack_start(image, False, False, padding = 12) - warning_hbox.pack_start(msg_vbox, False, False, padding = 12) - - self.vbox.pack_start(warning_hbox, padding=6) - - self.set_border_width(6) - self.set_has_separator(False) + x = cell_area.x + y = cell_area.y + self.checkbutton_height - 8 + w = cell_area.width + h = cell_area.height - self.set_default_response(gtk.RESPONSE_OK) - - self.set_transient_for(parent_window) - self.show_all() + #constrain operations to cell area, allowing for a 1 pixel border + #either side + #~ cairo_context.rectangle(x-1, y-1, w+2, h+2) + #~ cairo_context.clip() + + #fill in the background with dark grey + #this ensures that a selected cell's fill does not make + #the text impossible to read + #~ cairo_context.rectangle(x, y, w, h) + #~ cairo_context.set_source_rgb(0.267, 0.267, 0.267) + #~ cairo_context.fill() - self.connect('response', self.on_response) + #image width and height + image_w = self.image.size[0] + image_h = self.image.size[1] - def on_response(self, device_dialog, response): - show_again = self.show_again_checkbutton.get_active() - self.postChoiceCB(self, show_again) + #center the image horizontally + #bottom align vertically + #top left and right corners for the image: + image_x = x + ((w - image_w) / 2) + image_y = y + self.image_area_size - image_h -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.svg')) - # 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 photos or videos from?')) - prompt_label.set_line_wrap(True) - prompt_hbox = gtk.HBox() - prompt_hbox.pack_start(prompt_label, False, False, padding=6) - device_label = gtk.Label() - device_label.set_markup("<b>%s</b>" % 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("<i>%s</i>" % 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_hbox, padding=6) - self.vbox.pack_start(self.always_checkbutton, padding=6) + #convert PIL image to format suitable for cairo + image = create_cairo_image_surface(self.image, image_w, image_h) - self.set_border_width(6) - self.set_has_separator(False) + # draw a light grey border of 1px around the image + cairo_context.set_source_rgb(0.66, 0.66, 0.66) #light grey, #a9a9a9 + cairo_context.set_line_width(1) + cairo_context.rectangle(image_x-.5, image_y-.5, image_w+1, image_h+1) + cairo_context.stroke() - 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 RemoveAllJobCodeDialog(gtk.Dialog): - def __init__(self, parent_window, postChoiceCB): - gtk.Dialog.__init__(self, _('Remove all Job Codes?'), 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.svg')) + # draw a thin border around each cell + #~ cairo_context.set_source_rgb(0.33,0.33,0.33) + #~ cairo_context.rectangle(x, y, w, h) + #~ cairo_context.stroke() - prompt_hbox = gtk.HBox() + #place the image + cairo_context.set_source_surface(image, image_x, image_y) + cairo_context.paint() - icontheme = gtk.icon_theme_get_default() - icon = icontheme.load_icon('gtk-dialog-question', 36, gtk.ICON_LOOKUP_USE_BUILTIN) - if icon: - image = gtk.Image() - image.set_from_pixbuf(icon) - prompt_hbox.pack_start(image, False, False, padding = 6) - - prompt_label = gtk.Label(_('Should all Job Codes be removed?')) - prompt_label.set_line_wrap(True) - prompt_hbox.pack_start(prompt_label, False, False, padding=6) - - self.vbox.pack_start(prompt_hbox, padding=6) + #text + context = pangocairo.CairoContext(cairo_context) + + text_y = y + self.image_area_size + 10 + text_w = w - self.icon_width + text_x = x + self.icon_width + #~ context.rectangle(text_x, text_y, text_w, 15) + #~ context.clip() + + layout = context.create_layout() - self.set_border_width(6) - self.set_has_separator(False) + width = text_w * pango.SCALE + layout.set_width(width) - self.set_default_response(gtk.RESPONSE_OK) - - - self.set_transient_for(parent_window) - self.show_all() + layout.set_alignment(pango.ALIGN_CENTER) + layout.set_ellipsize(pango.ELLIPSIZE_END) + + #font color and size + fg_color = pango.AttrForeground(65535, 65535, 65535, 0, -1) + font_size = pango.AttrSize(8192, 0, -1) # 8 * 1024 = 8192 + font_family = pango.AttrFamily('sans', 0, -1) + attr = pango.AttrList() + attr.insert(fg_color) + attr.insert(font_size) + attr.insert(font_family) + layout.set_attributes(attr) + + layout.set_text(self.filename) + + context.move_to(text_x, text_y) + context.show_layout(layout) + #status + cairo_context.set_source_pixbuf(self.status, x, y + self.image_area_size + 10) + cairo_context.paint() - self.connect('response', self.on_response) + def do_get_size(self, widget, cell_area): + return (0, 0, self.image_area_size, self.image_area_size + self.text_area_size - self.checkbutton_height + 4) - def on_response(self, device_dialog, response): - userSelected = response == gtk.RESPONSE_OK - self.postChoiceCB(self, userSelected) + +gobject.type_register(ThumbnailCellRenderer) + + +class ThumbnailDisplay(gtk.IconView): + def __init__(self, parent_app): + gtk.IconView.__init__(self) + self.set_spacing(0) + self.set_row_spacing(5) + self.set_margin(25) + + self.rapid_app = parent_app + self.batch_size = 10 -class JobCodeDialog(gtk.Dialog): - """ Dialog prompting for a job code""" - - def __init__(self, parent_window, job_codes, default_job_code, postJobCodeEntryCB, autoStart, downloadSelected, entryOnly): - # 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.thumbnail_manager = ThumbnailManager(self.thumbnail_results, self.batch_size) + self.preview_manager = PreviewManager(self.preview_results) - self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) - self.postJobCodeEntryCB = postJobCodeEntryCB - self.autoStart = autoStart - self.downloadSelected = downloadSelected + self.treerow_index = {} + self.process_index = {} - 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) + self.rpd_files = {} - if len(job_codes) and not entryOnly: - # 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.total_thumbs_to_generate = 0 + self.thumbnails_generated = 0 - self.set_border_width(6) - self.set_has_separator(False) - - # make entry box have entry completion - self.entry = self.combobox.child + # dict of scan_pids that are having thumbnails generated + # value is the thumbnail process id + # this is needed when terminating thumbnailing early such as when + # user clicks download before the thumbnailing is finished + self.generating_thumbnails = {} - 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) + self.thumbnails = {} + self.previews = {} + self.previews_being_fetched = set() - # 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.stock_photo_thumbnails = tn.PhotoIcons() + self.stock_video_thumbnails = tn.VideoIcons() - self.vbox.pack_start(task_hbox, False, False, padding = 6) - self.vbox.pack_start(self.job_code_hbox, False, False, padding=12) + self.SELECTED_COL = 1 + self.UNIQUE_ID_COL = 2 + self.TIMESTAMP_COL = 4 + self.FILETYPE_COL = 5 + self.CHECKBUTTON_VISIBLE_COL = 6 + self.DOWNLOAD_STATUS_COL = 7 + self.STATUS_ICON_COL = 8 - self.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].lower().startswith(self.entry.get_text().lower()) - - def on_completion_match(self, completion, model, iter): - self.entry.set_text(model[iter][0]) - self.entry.set_position(-1) + self.liststore = gtk.ListStore( + gobject.TYPE_PYOBJECT, # 0 PIL thumbnail + gobject.TYPE_BOOLEAN, # 1 selected or not + str, # 2 unique id + str, # 3 file name + int, # 4 timestamp for sorting, converted float + int, # 5 file type i.e. photo or video + gobject.TYPE_BOOLEAN, # 6 visibility of checkbutton + int, # 7 status of download + gtk.gdk.Pixbuf, # 8 status icon + ) - def get_job_code(self): - return self.combobox.child.get_text() + self.clear() + self.set_model(self.liststore) - 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, self.downloadSelected) - - - -class SelectionTreeView(gtk.TreeView): - """ - TreeView display of photos and videos available for download - - Assumes a threaded environment. - """ - def __init__(self, parentApp): + + checkbutton = gtk.CellRendererToggle() + checkbutton.set_radio(False) + checkbutton.props.activatable = True + checkbutton.props.xalign = 0.0 + checkbutton.connect('toggled', self.on_checkbutton_toggled) + self.pack_end(checkbutton, expand=False) - self.parentApp = parentApp - self.rapidApp = parentApp.parentApp + self.add_attribute(checkbutton, "active", 1) + self.add_attribute(checkbutton, "visible", 6) - self.liststore = gtk.ListStore( - gtk.gdk.Pixbuf, # 0 thumbnail icon - str, # 1 name (for sorting) - int, # 2 timestamp (for sorting), float converted into an int - str, # 3 date (human readable) - int, # 4 size (for sorting) - str, # 5 size (human readable) - int, # 6 isImage (for sorting) - gtk.gdk.Pixbuf, # 7 type (photo or video) - str, # 8 job code - gobject.TYPE_PYOBJECT, # 9 mediaFile (for data) - gtk.gdk.Pixbuf, # 10 status icon - int, # 11 status (downloaded, cannot download, etc, for sorting) - str, # 12 path (on the device) - str, # 13 device - int) # 14 thread id (worker the file is associated with) - - self.selected_rows = set() - - # sort by date (unless there is a problem) - self.liststore.set_sort_column_id(2, gtk.SORT_ASCENDING) + checkbutton_size = checkbutton.get_size(self, None) + checkbutton_height = checkbutton_size[3] + checkbutton_width = checkbutton_size[2] - gtk.TreeView.__init__(self, self.liststore) + image = ThumbnailCellRenderer(checkbutton_height) + self.pack_start(image, expand=True) + self.add_attribute(image, "image", 0) + self.add_attribute(image, "filename", 3) + self.add_attribute(image, "status", 8) - selection = self.get_selection() - selection.set_mode(gtk.SELECTION_MULTIPLE) - selection.connect('changed', self.on_selection_changed) - - self.set_rubber_banding(True) - - # Status Column - # Indicates whether file was downloaded, or a warning or error of some kind - cell = gtk.CellRendererPixbuf() - cell.set_property("yalign", 0.5) - status_column = gtk.TreeViewColumn(_("Status"), cell, pixbuf=10) - status_column.set_sort_column_id(11) - status_column.connect('clicked', self.header_clicked) - self.append_column(status_column) - - # Type of file column i.e. photo or video (displays at user request) - cell = gtk.CellRendererPixbuf() - cell.set_property("yalign", 0.5) - self.type_column = gtk.TreeViewColumn(_("Type"), cell, pixbuf=7) - self.type_column.set_sort_column_id(6) - self.type_column.set_clickable(True) - self.type_column.connect('clicked', self.header_clicked) - self.append_column(self.type_column) - self.display_type_column(self.rapidApp.prefs.display_type_column) - - #File thumbnail column - if not DOWNLOAD_VIDEO: - title = _("Photo") - else: - title = _("File") - thumbnail_column = gtk.TreeViewColumn(title) - cellpb = gtk.CellRendererPixbuf() - if not DROP_SHADOW: - cellpb.set_fixed_size(60,50) - thumbnail_column.pack_start(cellpb, False) - thumbnail_column.set_attributes(cellpb, pixbuf=0) - thumbnail_column.set_sort_column_id(1) - thumbnail_column.set_clickable(True) - thumbnail_column.connect('clicked', self.header_clicked) - self.append_column(thumbnail_column) - - # Job code column - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - self.job_code_column = gtk.TreeViewColumn(_("Job Code"), cell, text=8) - self.job_code_column.set_sort_column_id(8) - self.job_code_column.set_resizable(True) - self.job_code_column.set_clickable(True) - self.job_code_column.connect('clicked', self.header_clicked) - self.append_column(self.job_code_column) - - # Date column - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - date_column = gtk.TreeViewColumn(_("Date"), cell, text=3) - date_column.set_sort_column_id(2) - date_column.set_resizable(True) - date_column.set_clickable(True) - date_column.connect('clicked', self.header_clicked) - self.append_column(date_column) - - # Size column (displays at user request) - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - self.size_column = gtk.TreeViewColumn(_("Size"), cell, text=5) - self.size_column.set_sort_column_id(4) - self.size_column.set_resizable(True) - self.size_column.set_clickable(True) - self.size_column.connect('clicked', self.header_clicked) - self.append_column(self.size_column) - self.display_size_column(self.rapidApp.prefs.display_size_column) - - # Device column (displays at user request) - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - self.device_column = gtk.TreeViewColumn(_("Device"), cell, text=13) - self.device_column.set_sort_column_id(13) - self.device_column.set_resizable(True) - self.device_column.set_clickable(True) - self.device_column.connect('clicked', self.header_clicked) - self.append_column(self.device_column) - self.display_device_column(self.rapidApp.prefs.display_device_column) - - # Filename column (displays at user request) - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - self.filename_column = gtk.TreeViewColumn(_("Filename"), cell, text=1) - self.filename_column.set_sort_column_id(1) - self.filename_column.set_resizable(True) - self.filename_column.set_clickable(True) - self.filename_column.connect('clicked', self.header_clicked) - self.append_column(self.filename_column) - self.display_filename_column(self.rapidApp.prefs.display_filename_column) - - # Path column (displays at user request) - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - self.path_column = gtk.TreeViewColumn(_("Path"), cell, text=12) - self.path_column.set_sort_column_id(12) - self.path_column.set_resizable(True) - self.path_column.set_clickable(True) - self.path_column.connect('clicked', self.header_clicked) - self.append_column(self.path_column) - self.display_path_column(self.rapidApp.prefs.display_path_column) - - self.show_all() + #set the background color to a darkish grey + self.modify_base(gtk.STATE_NORMAL, gtk.gdk.Color('#444444')) - # flag used to determine if a preview should be generated or not - # there is no point generating a preview for each photo when - # select all photos is called, for instance - self.suspend_previews = False + self.show_all() + self._setup_icons() - self.user_has_clicked_header = False + self.connect('item-activated', self.on_item_activated) + def _setup_icons(self): # icons to be displayed in status column - self.downloaded_icon = self.render_icon('rapid-photo-downloader-downloaded', gtk.ICON_SIZE_MENU) - self.download_failed_icon = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU) - self.error_icon = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU) - self.warning_icon = self.render_icon(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_MENU) - - self.download_pending_icon = self.render_icon('rapid-photo-downloader-download-pending', gtk.ICON_SIZE_MENU) - self.downloaded_with_warning_icon = self.render_icon('rapid-photo-downloader-downloaded-with-warning', gtk.ICON_SIZE_MENU) - self.downloaded_with_error_icon = self.render_icon('rapid-photo-downloader-downloaded-with-error', gtk.ICON_SIZE_MENU) + size = 16 + # standard icons + failed = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU) + self.download_failed_icon = failed.scale_simple(size, size, gtk.gdk.INTERP_HYPER) + error = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU) + self.error_icon = error.scale_simple(size, size, gtk.gdk.INTERP_HYPER) + warning = self.render_icon(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_MENU) + self.warning_icon = warning.scale_simple(size, size, gtk.gdk.INTERP_HYPER) + + # Rapid Photo Downloader specific icons + self.downloaded_icon = gtk.gdk.pixbuf_new_from_file_at_size( + paths.share_dir('glade3/rapid-photo-downloader-downloaded.svg'), + size, size) + self.download_pending_icon = gtk.gdk.pixbuf_new_from_file_at_size( + paths.share_dir('glade3/rapid-photo-downloader-download-pending.png'), + size, size) + self.downloaded_with_warning_icon = gtk.gdk.pixbuf_new_from_file_at_size( + paths.share_dir('glade3/rapid-photo-downloader-downloaded-with-warning.svg'), + size, size) + self.downloaded_with_error_icon = gtk.gdk.pixbuf_new_from_file_at_size( + paths.share_dir('glade3/rapid-photo-downloader-downloaded-with-error.svg'), + size, size) # make the not yet downloaded icon a transparent square self.not_downloaded_icon = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, 16, 16) self.not_downloaded_icon.fill(0xffffffff) self.not_downloaded_icon = self.not_downloaded_icon.add_alpha(True, chr(255), chr(255), chr(255)) - # but make it be a tick in the preview pane - self.not_downloaded_icon_tick = self.render_icon(gtk.STOCK_YES, gtk.ICON_SIZE_MENU) - - #preload generic icons - self.icon_photo = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/photo24.png')) - self.icon_video = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/video24.png')) - #with shadows - if DROP_SHADOW: - self.generic_photo_thumbnail = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/photo_small_shadow.png')) - self.generic_video_thumbnail = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/video_small_shadow.png')) - self.iconDropShadow = DropShadow(offset=(3,3), shadow = (0x34, 0x34, 0x34, 0xff), border=6) - else: - self.generic_photo_thumbnail = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/photo_small.png')) - self.generic_video_thumbnail = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/video_small.png')) - - self.previewed_file_treerowref = None - self.icontheme = gtk.icon_theme_get_default() - - - def get_thread(self, iter): - """ - Returns the thread associated with the liststore's iter - """ - return self.liststore.get_value(iter, 14) - - def get_status(self, iter): - """ - Returns the status associated with the liststore's iter - """ - return self.liststore.get_value(iter, 11) - - def get_mediaFile(self, iter): - """ - Returns the mediaFile associated with the liststore's iter - """ - return self.liststore.get_value(iter, 9) - - def get_is_image(self, iter): - """ - Returns the file type (is image or video) associated with the liststore's iter - """ - return self.liststore.get_value(iter, 6) - - def get_type_icon(self, iter): - """ - Returns the file type's pixbuf associated with the liststore's iter - """ - return self.liststore.get_value(iter, 7) - - def get_job_code(self, iter): - """ - Returns the job code associated with the liststore's iter - """ - return self.liststore.get_value(iter, 8) - - def get_status_icon(self, status, preview=False): + def get_status_icon(self, status): """ Returns the correct icon, based on the status """ @@ -3679,10 +577,7 @@ class SelectionTreeView(gtk.TreeView): elif status == STATUS_DOWNLOADED: status_icon = self.downloaded_icon elif status == STATUS_NOT_DOWNLOADED: - if preview: - status_icon = self.not_downloaded_icon_tick - else: - status_icon = self.not_downloaded_icon + status_icon = self.not_downloaded_icon elif status in [STATUS_DOWNLOADED_WITH_WARNING, STATUS_BACKUP_PROBLEM]: status_icon = self.downloaded_with_warning_icon elif status in [STATUS_DOWNLOAD_FAILED, STATUS_DOWNLOAD_AND_BACKUP_FAILED]: @@ -3690,1469 +585,1412 @@ class SelectionTreeView(gtk.TreeView): elif status == STATUS_DOWNLOAD_PENDING: status_icon = self.download_pending_icon else: - sys.stderr.write("FIXME: unknown status: %s\n" % status) + logger.critical("FIXME: unknown status: %s", status) status_icon = self.not_downloaded_icon - return status_icon - - def get_tree_row_refs(self): - """ - Returns a list of all tree row references - """ - tree_row_refs = [] - iter = self.liststore.get_iter_first() - while iter: - tree_row_refs.append(self.get_mediaFile(iter).treerowref) - iter = self.liststore.iter_next(iter) - return tree_row_refs - - def get_selected_tree_row_refs(self): - """ - Returns a list of tree row references for the selected rows - """ - tree_row_refs = [] - selection = self.get_selection() - model, pathlist = selection.get_selected_rows() - for path in pathlist: - iter = self.liststore.get_iter(path) - tree_row_refs.append(self.get_mediaFile(iter).treerowref) - return tree_row_refs - - def get_tree_row_iters(self, selected_only=False): - """ - Yields tree row iters + return status_icon + + def sort_by_timestamp(self): + self.liststore.set_sort_column_id(self.TIMESTAMP_COL, gtk.SORT_ASCENDING) - If selected_only is True, then only those from the selected - rows will be returned. + def on_checkbutton_toggled(self, cellrenderertoggle, path): + iter = self.liststore.get_iter(path) + self.liststore.set_value(iter, self.SELECTED_COL, not cellrenderertoggle.get_active()) + self.rapid_app.set_download_action_sensitivity() - This function is essential when modifying any content - in the list store (because rows can easily be moved when their - content changes) - """ - if selected_only: - tree_row_refs = self.get_selected_tree_row_refs() - else: - tree_row_refs = self.get_tree_row_refs() - for reference in tree_row_refs: - path = reference.get_path() - yield self.liststore.get_iter(path) + def set_selected(self, unique_id, value): + iter = self.get_iter_from_unique_id(unique_id) + self.liststore.set_value(iter, self.SELECTED_COL, value) - def add_file(self, mediaFile): - if debug_info: - cmd_line('Adding file %s' % mediaFile.fullFileName) - - # metadata is loaded when previews are generated before downloading - if mediaFile.metadata: - date = mediaFile.dateTime() - timestamp = mediaFile.metadata.timeStamp(missing=None) - if timestamp is None: - timestamp = mediaFile.modificationTime - # if metadata has not been loaded, substitute other values - else: - timestamp = mediaFile.modificationTime - date = datetime.datetime.fromtimestamp(timestamp) + def add_file(self, rpd_file, generate_thumbnail): - timestamp = int(timestamp) - - date_human_readable = date_time_human_readable(date) - name = mediaFile.name - size = mediaFile.size - thumbnail = mediaFile.thumbnail - - if mediaFile.genericThumbnail: - if mediaFile.isImage: - thumbnail_icon = self.generic_photo_thumbnail - else: - thumbnail_icon = self.generic_video_thumbnail - else: - thumbnail_icon = common.scale2pixbuf(60, 36, thumbnail) - - if DROP_SHADOW and not mediaFile.genericThumbnail: - pil_image = pixbuf_to_image(thumbnail_icon) - pil_image = self.iconDropShadow.dropShadow(pil_image) - thumbnail_icon = image_to_pixbuf(pil_image) + thumbnail_icon = self.get_stock_icon(rpd_file.file_type) + unique_id = rpd_file.unique_id + scan_pid = rpd_file.scan_pid + timestamp = int(rpd_file.modification_time) + + iter = self.liststore.append((thumbnail_icon, + True, + unique_id, + rpd_file.display_name, + timestamp, + rpd_file.file_type, + True, + STATUS_NOT_DOWNLOADED, + self.not_downloaded_icon + )) - if mediaFile.isImage: - type_icon = self.icon_photo - else: - type_icon = self.icon_video - - status_icon = self.get_status_icon(mediaFile.status) - - if debug_info and False: - cmd_line('Thumbnail icon: %s' % thumbnail_icon) - cmd_line('Name: %s' % name) - cmd_line('Timestamp: %s' % timestamp) - cmd_line('Date: %s' % date_human_readable) - cmd_line('Size: %s %s' % (size, common.formatSizeForUser(size))) - cmd_line('Is an image: %s' % mediaFile.isImage) - cmd_line('Status: %s' % self.status_human_readable(mediaFile)) - cmd_line('Path: %s' % mediaFile.path) - cmd_line('Device name: %s' % mediaFile.deviceName) - cmd_line('Thread: %s' % mediaFile.thread_id) - cmd_line(' ') - - iter = self.liststore.append((thumbnail_icon, name, timestamp, date_human_readable, size, common.formatSizeForUser(size), mediaFile.isImage, type_icon, '', mediaFile, status_icon, mediaFile.status, mediaFile.path, mediaFile.deviceName, mediaFile.thread_id)) - - #create a reference to this row and store it in the mediaFile path = self.liststore.get_path(iter) - mediaFile.treerowref = gtk.TreeRowReference(self.liststore, path) + treerowref = gtk.TreeRowReference(self.liststore, path) - if mediaFile.status in [STATUS_CANNOT_DOWNLOAD, STATUS_WARNING]: - if not self.user_has_clicked_header: - self.liststore.set_sort_column_id(11, gtk.SORT_DESCENDING) + if scan_pid in self.process_index: + self.process_index[scan_pid].append(unique_id) + else: + self.process_index[scan_pid] = [unique_id,] + + self.treerow_index[unique_id] = treerowref + self.rpd_files[unique_id] = rpd_file - def no_selected_rows_available_for_download(self): - """ - Gets the number of rows the user has selected that can actually - be downloaded, and the threads they are found in - """ - v = 0 - threads = [] - model, paths = self.get_selection().get_selected_rows() - for path in paths: - iter = self.liststore.get_iter(path) - status = self.get_status(iter) - if status in [STATUS_NOT_DOWNLOADED, STATUS_WARNING]: - v += 1 - thread = self.get_thread(iter) - if thread not in threads: - threads.append(thread) - return v, threads - - def rows_available_for_download(self): - """ - Returns true if one or more rows has their status as STATUS_NOT_DOWNLOADED or STATUS_WARNING - """ - iter = self.liststore.get_iter_first() - while iter: - status = self.get_status(iter) - if status in [STATUS_NOT_DOWNLOADED, STATUS_WARNING]: - return True - iter = self.liststore.iter_next(iter) - return False + if generate_thumbnail: + self.total_thumbs_to_generate += 1 + + def get_sample_file(self, file_type): + """Returns an rpd_file for of a given file type, or None if it does + not exist""" + for unique_id, rpd_file in self.rpd_files.iteritems(): + if rpd_file.file_type == file_type: + if rpd_file.status <> STATUS_CANNOT_DOWNLOAD: + return rpd_file + + return None + + def get_unique_id_from_iter(self, iter): + return self.liststore.get_value(iter, 2) + + def get_iter_from_unique_id(self, unique_id): + treerowref = self.treerow_index[unique_id] + path = treerowref.get_path() + return self.liststore.get_iter(path) - def update_download_selected_button(self): + def on_item_activated(self, iconview, path): """ - Updates the text on the Download Selection button, and set its sensitivity """ - no_available_for_download = 0 - selection = self.get_selection() - model, paths = selection.get_selected_rows() - if paths: - path = paths[0] + iter = self.liststore.get_iter(path) + self.show_preview(iter=iter) + self.advance_get_preview_image(iter) + + + def _get_preview(self, unique_id, rpd_file): + if unique_id not in self.previews_being_fetched: + #check if preview should be from a downloaded file, or the source + if rpd_file.status in DOWNLOADED: + file_location = rpd_file.download_full_file_name + else: + file_location = rpd_file.full_file_name + self.preview_manager.get_preview(unique_id, file_location, + rpd_file.file_type, size_max=None,) + + self.previews_being_fetched.add(unique_id) + + def show_preview(self, unique_id=None, iter=None): + if unique_id is not None: + iter = self.get_iter_from_unique_id(unique_id) + elif iter is not None: + unique_id = self.get_unique_id_from_iter(iter) + else: + # neither an iter or a unique_id were passed + # use iter from first selected file + # if none is selected, choose the first file + selected = self.get_selected_items() + if selected: + path = selected[0] + else: + path = 0 iter = self.liststore.get_iter(path) + unique_id = self.get_unique_id_from_iter(iter) - #update button text - no_available_for_download, threads = self.no_selected_rows_available_for_download() - if no_available_for_download and workers.scanComplete(threads): - self.rapidApp.download_selected_button.set_label(self.rapidApp.DOWNLOAD_SELECTED_LABEL + " (%s)" % no_available_for_download) - self.rapidApp.download_selected_button.set_sensitive(True) - else: - #nothing was selected, or nothing is available from what the user selected, or should not download right now - self.rapidApp.download_selected_button.set_label(self.rapidApp.DOWNLOAD_SELECTED_LABEL) - self.rapidApp.download_selected_button.set_sensitive(False) - - def on_selection_changed(self, selection): - """ - Update download selected button and preview the most recently - selected row in the treeview - """ - self.update_download_selected_button() - size = selection.count_selected_rows() - if size == 0: - self.selected_rows = set() - self.show_preview(None) + rpd_file = self.rpd_files[unique_id] + + if unique_id in self.previews: + preview_image = self.previews[unique_id] else: - if size <= len(self.selected_rows): - # discard everything, start over - self.selected_rows = set() - self.selection_size = size - model, paths = selection.get_selected_rows() - for path in paths: - iter = self.liststore.get_iter(path) - ref = self.get_mediaFile(iter).treerowref - - if ref not in self.selected_rows: - self.show_preview(iter) - self.selected_rows.add(ref) + # request daemon process to get a full size thumbnail + self._get_preview(unique_id, rpd_file) + if unique_id in self.thumbnails: + preview_image = self.thumbnails[unique_id] + else: + preview_image = self.get_stock_icon(rpd_file.file_type) + + checked = self.liststore.get_value(iter, self.SELECTED_COL) + include_checkbutton_visible = rpd_file.status == STATUS_NOT_DOWNLOADED + self.rapid_app.show_preview_image(unique_id, preview_image, + include_checkbutton_visible, checked) - def clear_all(self, thread_id = None): - if thread_id is None: - self.liststore.clear() - self.show_preview(None) - else: + def _get_next_iter(self, iter): + iter = self.liststore.iter_next(iter) + if iter is None: iter = self.liststore.get_iter_first() - while iter: - t = self.get_thread(iter) - if t == thread_id: - if self.previewed_file_treerowref: - mediaFile = self.get_mediaFile(iter) - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(None) - self.liststore.remove(iter) - # need to start over, or else bad things happen - iter = self.liststore.get_iter_first() - else: - iter = self.liststore.iter_next(iter) + return iter + + def _get_prev_iter(self, iter): + row = self.liststore.get_path(iter)[0] + if row == 0: + row = len(self.liststore)-1 + else: + row -= 1 + iter = self.liststore.get_iter(row) + return iter - def refreshSampleDownloadFolders(self, thread_id = None): + def show_next_image(self, unique_id): + iter = self.get_iter_from_unique_id(unique_id) + iter = self._get_next_iter(iter) + + if iter is not None: + self.show_preview(iter=iter) + + # cache next image + self.advance_get_preview_image(iter, prev=False, next=True) + + def show_prev_image(self, unique_id): + iter = self.get_iter_from_unique_id(unique_id) + iter = self._get_prev_iter(iter) + + if iter is not None: + self.show_preview(iter=iter) + + # cache next image + self.advance_get_preview_image(iter, prev=True, next=False) + + + def advance_get_preview_image(self, iter, prev=True, next=True): + unique_ids = [] + if next: + next_iter = self._get_next_iter(iter) + unique_ids.append(self.get_unique_id_from_iter(next_iter)) + + if prev: + prev_iter = self._get_prev_iter(iter) + unique_ids.append(self.get_unique_id_from_iter(prev_iter)) + + for unique_id in unique_ids: + if not unique_id in self.previews: + rpd_file = self.rpd_files[unique_id] + self._get_preview(unique_id, rpd_file) + + def check_all(self, check_all, file_type=None): + for row in self.liststore: + if row[self.CHECKBUTTON_VISIBLE_COL]: + if file_type is not None: + if row[self.FILETYPE_COL] == file_type: + row[self.SELECTED_COL] = check_all + else: + row[self.SELECTED_COL] = check_all + self.rapid_app.set_download_action_sensitivity() + + def files_are_checked_to_download(self): """ - Refreshes the download folder of every file that has not yet been downloaded + Returns True if there is any file that the user has indicated they + intend to download, else returns False. + """ + for row in self.liststore: + if row[self.SELECTED_COL]: + rpd_file = self.rpd_files[row[self.UNIQUE_ID_COL]] + if rpd_file.status not in DOWNLOADED: + return True + return False - This is useful when the user updates the preferences, and the scan has already occurred (or is occurring) + def get_files_checked_for_download(self, scan_pid): + """ + Returns a dict of scan ids and associated files the user has indicated + they want to download - If thread_id is specified, will only update rows with that thread + If scan_pid is not None, then returns only those files from that scan_pid """ - for iter in self.get_tree_row_iters(): - status = self.get_status(iter) - if status in [STATUS_NOT_DOWNLOADED, STATUS_WARNING, STATUS_CANNOT_DOWNLOAD]: - regenerate = True - if thread_id is not None: - t = self.get_thread(iter) - regenerate = t == thread_id + files = dict() + if scan_pid is None: + for row in self.liststore: + if row[self.SELECTED_COL]: + rpd_file = self.rpd_files[row[self.UNIQUE_ID_COL]] + if rpd_file.status not in DOWNLOADED: + scan_pid = rpd_file.scan_pid + if scan_pid in files: + files[scan_pid].append(rpd_file) + else: + files[scan_pid] = [rpd_file,] + else: + files[scan_pid] = [] + for unique_id in self.process_index[scan_pid]: + rpd_file = self.rpd_files[unique_id] + if rpd_file.status not in DOWNLOADED: + iter = self.get_iter_from_unique_id(unique_id) + if self.liststore.get_value(iter, self.SELECTED_COL): + files[scan_pid].append(rpd_file) + return files - if regenerate: - mediaFile = self.get_mediaFile(iter) - if mediaFile.isImage: - mediaFile.downloadFolder = self.rapidApp.prefs.download_folder - else: - mediaFile.downloadFolder = self.rapidApp.prefs.video_download_folder - mediaFile.samplePath = os.path.join(mediaFile.downloadFolder, mediaFile.sampleSubfolder) - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(iter) - - def _refreshNameFactories(self): - sample_download_start_time = datetime.datetime.now() - self.imageRenamePrefsFactory = rn.ImageRenamePreferences(self.rapidApp.prefs.image_rename, self, - self.rapidApp.fileSequenceLock, sequences) - self.imageRenamePrefsFactory.setDownloadStartTime(sample_download_start_time) - self.videoRenamePrefsFactory = rn.VideoRenamePreferences(self.rapidApp.prefs.video_rename, self, - self.rapidApp.fileSequenceLock, sequences) - self.videoRenamePrefsFactory.setDownloadStartTime(sample_download_start_time) - self.subfolderPrefsFactory = rn.SubfolderPreferences(self.rapidApp.prefs.subfolder, self) - self.subfolderPrefsFactory.setDownloadStartTime(sample_download_start_time) - self.videoSubfolderPrefsFactory = rn.VideoSubfolderPreferences(self.rapidApp.prefs.video_subfolder, self) - self.videoSubfolderPrefsFactory.setDownloadStartTime(sample_download_start_time) - self.strip_characters = self.rapidApp.prefs.strip_characters - - - def refreshGeneratedSampleSubfolderAndName(self, thread_id = None): + def get_no_files_remaining(self, scan_pid): """ - Refreshes the name, subfolder and status of every file that has not yet been downloaded - - This is useful when the user updates the preferences, and the scan has already occurred (or is occurring) + Returns the number of files that have not yet been downloaded for the + scan_pid + """ + i = 0 + for unique_id in self.process_index[scan_pid]: + rpd_file = self.rpd_files[unique_id] + if rpd_file.status == STATUS_NOT_DOWNLOADED: + i += 1 + return i - If thread_id is specified, will only update rows with that thread + def files_remain_to_download(self): + """ + Returns True if any files remain that are not downloaded, else returns + False + """ + for row in self.liststore: + if row[self.DOWNLOAD_STATUS_COL] == STATUS_NOT_DOWNLOADED: + return True + return False + + + def mark_download_pending(self, files_by_scan_pid): """ - self._setUsesJobCode() - self._refreshNameFactories() - for iter in self.get_tree_row_iters(): - status = self.get_status(iter) - if status in [STATUS_NOT_DOWNLOADED, STATUS_WARNING, STATUS_CANNOT_DOWNLOAD]: - regenerate = True - if thread_id is not None: - t = self.get_thread(iter) - regenerate = t == thread_id + Sets status to download pending and updates thumbnails display + """ + for scan_pid in files_by_scan_pid: + for rpd_file in files_by_scan_pid[scan_pid]: + unique_id = rpd_file.unique_id + self.rpd_files[unique_id].status = STATUS_DOWNLOAD_PENDING + iter = self.get_iter_from_unique_id(unique_id) + if not self.rapid_app.auto_start_is_on: + # don't make the checkbox invisible immediately when on auto start + # otherwise the box can be rendred at the wrong size, as it is + # realized after the checkbox has already been made invisible + self.liststore.set_value(iter, self.CHECKBUTTON_VISIBLE_COL, False) + self.liststore.set_value(iter, self.SELECTED_COL, False) + self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, STATUS_DOWNLOAD_PENDING) + icon = self.get_status_icon(STATUS_DOWNLOAD_PENDING) + self.liststore.set_value(iter, self.STATUS_ICON_COL, icon) - if regenerate: - mediaFile = self.get_mediaFile(iter) - self.generateSampleSubfolderAndName(mediaFile, iter) - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(iter) - - def generateSampleSubfolderAndName(self, mediaFile, iter): - problem = pn.Problem() - if mediaFile.isImage: - fallback_date = None - subfolderPrefsFactory = self.subfolderPrefsFactory - renamePrefsFactory = self.imageRenamePrefsFactory - nameUsesJobCode = self.imageRenameUsesJobCode - subfolderUsesJobCode = self.imageSubfolderUsesJobCode + def select_image(self, unique_id): + iter = self.get_iter_from_unique_id(unique_id) + path = self.liststore.get_path(iter) + self.select_path(path) + self.scroll_to_path(path, use_align=False, row_align=0.5, col_align=0.5) + + def get_stock_icon(self, file_type): + if file_type == rpdfile.FILE_TYPE_PHOTO: + return self.stock_photo_thumbnails.stock_thumbnail_image_icon else: - fallback_date = mediaFile.modificationTime - subfolderPrefsFactory = self.videoSubfolderPrefsFactory - renamePrefsFactory = self.videoRenamePrefsFactory - nameUsesJobCode = self.videoRenameUsesJobCode - subfolderUsesJobCode = self.videoSubfolderUsesJobCode + return self.stock_video_thumbnails.stock_thumbnail_image_icon - renamePrefsFactory.setJobCode(self.get_job_code(iter)) - subfolderPrefsFactory.setJobCode(self.get_job_code(iter)) - - generateSubfolderAndName(mediaFile, problem, subfolderPrefsFactory, renamePrefsFactory, - nameUsesJobCode, subfolderUsesJobCode, - self.strip_characters, fallback_date) - if self.get_status(iter) != mediaFile.status: - self.liststore.set(iter, 11, mediaFile.status) - self.liststore.set(iter, 10, self.get_status_icon(mediaFile.status)) - mediaFile.sampleStale = False - - def _setUsesJobCode(self): - self.imageRenameUsesJobCode = rn.usesJobCode(self.rapidApp.prefs.image_rename) - self.imageSubfolderUsesJobCode = rn.usesJobCode(self.rapidApp.prefs.subfolder) - self.videoRenameUsesJobCode = rn.usesJobCode(self.rapidApp.prefs.video_rename) - self.videoSubfolderUsesJobCode = rn.usesJobCode(self.rapidApp.prefs.video_subfolder) + def update_status_post_download(self, rpd_file): + iter = self.get_iter_from_unique_id(rpd_file.unique_id) + self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, rpd_file.status) + icon = self.get_status_icon(rpd_file.status) + self.liststore.set_value(iter, self.STATUS_ICON_COL, icon) + self.liststore.set_value(iter, self.CHECKBUTTON_VISIBLE_COL, False) + self.rpd_files[rpd_file.unique_id] = rpd_file + + def generate_thumbnails(self, scan_pid): + """Initiate thumbnail generation for files scanned in one process + """ + rpd_files = [self.rpd_files[unique_id] for unique_id in self.process_index[scan_pid]] + thumbnail_pid = self.thumbnail_manager.add_task((scan_pid, rpd_files)) + self.generating_thumbnails[scan_pid] = thumbnail_pid + def _set_thumbnail(self, unique_id, icon): + treerowref = self.treerow_index[unique_id] + path = treerowref.get_path() + iter = self.liststore.get_iter(path) + self.liststore.set(iter, 0, icon) - def status_human_readable(self, mediaFile): - if mediaFile.status == STATUS_DOWNLOADED: - v = _('%(filetype)s was downloaded successfully') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_DOWNLOAD_FAILED: - v = _('%(filetype)s was not downloaded') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_DOWNLOADED_WITH_WARNING: - v = _('%(filetype)s was downloaded with warnings') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_BACKUP_PROBLEM: - v = _('%(filetype)s was downloaded but there were problems backing up') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_DOWNLOAD_AND_BACKUP_FAILED: - v = _('%(filetype)s was neither downloaded nor backed up') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_NOT_DOWNLOADED: - v = _('%(filetype)s is ready to be downloaded') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_DOWNLOAD_PENDING: - v = _('%(filetype)s is about to be downloaded') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_WARNING: - v = _('%(filetype)s will be downloaded with warnings')% {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_CANNOT_DOWNLOAD: - v = _('%(filetype)s cannot be downloaded') % {'filetype': mediaFile.displayNameCap} - return v - - def show_preview(self, iter): + def update_thumbnail(self, thumbnail_data): + """ + Takes the generated thumbnail and updates the display + + If the thumbnail_data includes a second image, that is used to + update the thumbnail list using the unique_id + """ + unique_id = thumbnail_data[0] + thumbnail_icon = thumbnail_data[1] + + if thumbnail_icon is not None: + # get the thumbnail icon in PIL format + thumbnail_icon = thumbnail_icon.get_image() - if not iter: - # clear everything except the label Preview at the top - for widget in [self.parentApp.preview_original_name_label, - self.parentApp.preview_name_label, - self.parentApp.preview_status_label, - self.parentApp.preview_problem_title_label, - self.parentApp.preview_problem_label]: - widget.set_text('') - - for widget in [self.parentApp.preview_image, - self.parentApp.preview_name_label, - self.parentApp.preview_original_name_label, - self.parentApp.preview_status_label, - self.parentApp.preview_problem_title_label, - self.parentApp.preview_problem_label - ]: - widget.set_tooltip_text('') + if thumbnail_icon: + self._set_thumbnail(unique_id, thumbnail_icon) - self.parentApp.preview_image.clear() - self.parentApp.preview_status_icon.clear() - self.parentApp.preview_destination_expander.hide() - self.parentApp.preview_device_expander.hide() - self.previewed_file_treerowref = None - + if len(thumbnail_data) > 2: + # get the 2nd image in PIL format + self.thumbnails[unique_id] = thumbnail_data[2].get_image() + + def terminate_thumbnail_generation(self, scan_pid): + """ + Terminates thumbnail generation if thumbnails are currently + being generated for this scan_pid + """ - elif not self.suspend_previews: - mediaFile = self.get_mediaFile(iter) + if scan_pid in self.generating_thumbnails: + terminated = True + self.thumbnail_manager.terminate_process( + self.generating_thumbnails[scan_pid]) + del self.generating_thumbnails[scan_pid] - self.previewed_file_treerowref = mediaFile.treerowref + if len(self.generating_thumbnails) == 0: + self._reset_thumbnail_tracking_and_display() + else: + terminated = False - self.parentApp.set_base_preview_image(mediaFile.thumbnail) - thumbnail = self.parentApp.scaledPreviewImage() + return terminated - self.parentApp.preview_image.set_from_pixbuf(thumbnail) + def mark_thumbnails_needed(self, rpd_files): + for rpd_file in rpd_files: + if rpd_file.unique_id not in self.thumbnails: + rpd_file.generate_thumbnail = True + + def _reset_thumbnail_tracking_and_display(self): + self.rapid_app.download_progressbar.set_fraction(0.0) + self.rapid_app.download_progressbar.set_text('') + self.thumbnails_generated = 0 + self.total_thumbs_to_generate = 0 + + def thumbnail_results(self, source, condition): + connection = self.thumbnail_manager.get_pipe(source) + + conn_type, data = connection.recv() + + if conn_type == rpdmp.CONN_COMPLETE: + scan_pid = data + del self.generating_thumbnails[scan_pid] + connection.close() + return False + else: - image_tool_tip = "%s\n%s" % (date_time_human_readable(mediaFile.dateTime(), False), common.formatSizeForUser(mediaFile.size)) - self.parentApp.preview_image.set_tooltip_text(image_tool_tip) - - if mediaFile.sampleStale and mediaFile.status in [STATUS_NOT_DOWNLOADED, STATUS_WARNING]: - self._refreshNameFactories() - self._setUsesJobCode() - self.generateSampleSubfolderAndName(mediaFile, iter) - - self.parentApp.preview_original_name_label.set_text(mediaFile.name) - self.parentApp.preview_original_name_label.set_tooltip_text(mediaFile.name) - if mediaFile.volume: - pixbuf = mediaFile.volume.get_icon_pixbuf(16) - else: - pixbuf = self.icontheme.load_icon('gtk-harddisk', 16, gtk.ICON_LOOKUP_USE_BUILTIN) - self.parentApp.preview_device_image.set_from_pixbuf(pixbuf) - self.parentApp.preview_device_label.set_text(mediaFile.deviceName) - self.parentApp.preview_device_path_label.set_text(mediaFile.path) - self.parentApp.preview_device_path_label.set_tooltip_text(mediaFile.path) + for thumbnail_data in data: + self.update_thumbnail(thumbnail_data) - if using_gio: - folder = gio.File(mediaFile.downloadFolder) - fileInfo = folder.query_info(gio.FILE_ATTRIBUTE_STANDARD_ICON) - icon = fileInfo.get_icon() - pixbuf = common.get_icon_pixbuf(using_gio, icon, 16, fallback='folder') - else: - pixbuf = self.icontheme.load_icon('folder', 16, gtk.ICON_LOOKUP_USE_BUILTIN) - - self.parentApp.preview_destination_image.set_from_pixbuf(pixbuf) - downloadFolderName = os.path.split(mediaFile.downloadFolder)[1] - self.parentApp.preview_destination_label.set_text(downloadFolderName) - - if mediaFile.status in [STATUS_WARNING, STATUS_CANNOT_DOWNLOAD, STATUS_NOT_DOWNLOADED, STATUS_DOWNLOAD_PENDING]: - - self.parentApp.preview_name_label.set_text(mediaFile.sampleName) - self.parentApp.preview_name_label.set_tooltip_text(mediaFile.sampleName) - self.parentApp.preview_destination_path_label.set_text(mediaFile.samplePath) - self.parentApp.preview_destination_path_label.set_tooltip_text(mediaFile.samplePath) - else: - self.parentApp.preview_name_label.set_text(mediaFile.downloadName) - self.parentApp.preview_name_label.set_tooltip_text(mediaFile.downloadName) - self.parentApp.preview_destination_path_label.set_text(mediaFile.downloadPath) - self.parentApp.preview_destination_path_label.set_tooltip_text(mediaFile.downloadPath) + self.thumbnails_generated += len(data) - status_text = self.status_human_readable(mediaFile) - self.parentApp.preview_status_icon.set_from_pixbuf(self.get_status_icon(mediaFile.status, preview=True)) - self.parentApp.preview_status_label.set_markup('<b>' + status_text + '</b>') - self.parentApp.preview_status_label.set_tooltip_text(status_text) - - - if mediaFile.status in [STATUS_WARNING, STATUS_DOWNLOAD_FAILED, - STATUS_DOWNLOADED_WITH_WARNING, - STATUS_CANNOT_DOWNLOAD, - STATUS_BACKUP_PROBLEM, - STATUS_DOWNLOAD_AND_BACKUP_FAILED]: - problem_title = mediaFile.problem.get_title() - self.parentApp.preview_problem_title_label.set_markup('<i>' + problem_title + '</i>') - self.parentApp.preview_problem_title_label.set_tooltip_text(problem_title) - - problem_text = mediaFile.problem.get_problems() - self.parentApp.preview_problem_label.set_text(problem_text) - self.parentApp.preview_problem_label.set_tooltip_text(problem_text) + # clear progress bar information if all thumbnails have been + # extracted + if self.thumbnails_generated == self.total_thumbs_to_generate: + self._reset_thumbnail_tracking_and_display() else: - self.parentApp.preview_problem_label.set_markup('') - self.parentApp.preview_problem_title_label.set_markup('') - for widget in [self.parentApp.preview_problem_title_label, - self.parentApp.preview_problem_label - ]: - widget.set_tooltip_text('') - - if self.rapidApp.prefs.display_preview_folders: - self.parentApp.preview_destination_expander.show() - self.parentApp.preview_device_expander.show() - - - def select_rows(self, range): - selection = self.get_selection() - if range == 'all': - selection.select_all() - elif range == 'none': - selection.unselect_all() - else: - # User chose to select all photos or all videos, - # or select all files with or without job codes. - - # Temporarily suspend previews while a large number of rows - # are being selected / unselected - self.suspend_previews = True - - iter = self.liststore.get_iter_first() - while iter is not None: - if range in ['photos', 'videos']: - type = self.get_is_image(iter) - select_row = (type and range == 'photos') or (not type and range == 'videos') - else: - job_code = self.get_job_code(iter) - select_row = (job_code and range == 'withjobcode') or (not job_code and range == 'withoutjobcode') - - if select_row: - selection.select_iter(iter) - else: - selection.unselect_iter(iter) - iter = self.liststore.iter_next(iter) + if self.total_thumbs_to_generate: + self.rapid_app.download_progressbar.set_fraction( + float(self.thumbnails_generated) / self.total_thumbs_to_generate) - self.suspend_previews = False - # select the first photo / video - iter = self.liststore.get_iter_first() - while iter is not None: - type = self.get_is_image(iter) - if (type and range == 'photos') or (not type and range == 'videos'): - self.show_preview(iter) - break - iter = self.liststore.iter_next(iter) - - - def header_clicked(self, column): - self.user_has_clicked_header = True - def display_filename_column(self, display): + return True + + def preview_results(self, unique_id, preview_full_size, preview_small): """ - if display is true, the column will be shown - otherwise, it will not be shown + Receive a full size preview image and update """ - self.filename_column.set_visible(display) + self.previews_being_fetched.remove(unique_id) + if preview_full_size: + preview_image = preview_full_size.get_image() + self.previews[unique_id] = preview_image + self.rapid_app.update_preview_image(unique_id, preview_image) + + # user can turn off option for thumbnail generation after a scan + if unique_id not in self.thumbnails: + self._set_thumbnail(unique_id, preview_small.get_image()) + + + def clear_all(self, scan_pid=None, keep_downloaded_files=False): + """ + Removes files from display and internal tracking. - def display_size_column(self, display): - self.size_column.set_visible(display) + If scan_pid is not None, then only files matching that scan_pid will + be removed. Otherwise, everything will be removed. + + If keep_downloaded_files is True, files will not be removed if they + have been downloaded. + """ + if scan_pid is None and not keep_downloaded_files: + self.liststore.clear() + self.treerow_index = {} + self.process_index = {} + + self.rpd_files = {} + else: + if scan_pid in self.process_index: + for unique_id in self.process_index[scan_pid]: + rpd_file = self.rpd_files[unique_id] + if not keep_downloaded_files or not rpd_file.status in DOWNLOADED: + treerowref = self.treerow_index[rpd_file.unique_id] + path = treerowref.get_path() + iter = self.liststore.get_iter(path) + self.liststore.remove(iter) + del self.treerow_index[rpd_file.unique_id] + del self.rpd_files[rpd_file.unique_id] + if not keep_downloaded_files or not len(self.process_index[scan_pid]): + del self.process_index[scan_pid] + +class TaskManager: + def __init__(self, results_callback, batch_size): + self.results_callback = results_callback + + # List of actual process, it's terminate_queue, and it's run_event + self._processes = [] + + self._pipes = {} + self.batch_size = batch_size + + self.paused = False + self.no_tasks = 0 + + + def add_task(self, task): + pid = self._setup_task(task) + logger.debug("TaskManager PID: %s", pid) + self.no_tasks += 1 + return pid - def display_type_column(self, display): - if not DOWNLOAD_VIDEO: - self.type_column.set_visible(False) - else: - self.type_column.set_visible(display) - def display_path_column(self, display): - self.path_column.set_visible(display) + def _setup_task(self, task): + task_results_conn, task_process_conn = self._setup_pipe() - def display_device_column(self, display): - self.device_column.set_visible(display) + source = task_results_conn.fileno() + self._pipes[source] = task_results_conn + gobject.io_add_watch(source, gobject.IO_IN, self.results_callback) - def apply_job_code(self, job_code, overwrite=True, to_all_rows=False, thread_id=None): - """ - Applies the Job code to the selected rows, or all rows if to_all_rows is True. + terminate_queue = Queue() + run_event = Event() + run_event.set() - If overwrite is True, then it will overwrite any existing job code. - """ - - def _apply_job_code(): - status = self.get_status(iter) - if status in [STATUS_DOWNLOAD_PENDING, STATUS_WARNING, STATUS_NOT_DOWNLOADED]: + return self._initiate_task(task, task_results_conn, task_process_conn, + terminate_queue, run_event) + + def _setup_pipe(self): + return Pipe(duplex=False) + + def _initiate_task(self, task, task_process_conn, terminate_queue, run_event): + logger.error("Implement child class method!") + + + def processes(self): + for i in range(len(self._processes)): + yield self._processes[i] + + def start(self): + self.paused = False + for scan in self.processes(): + run_event = scan[2] + if not run_event.is_set(): + run_event.set() - if mediaFile.isImage: - apply = rn.usesJobCode(self.rapidApp.prefs.image_rename) or rn.usesJobCode(self.rapidApp.prefs.subfolder) - else: - apply = rn.usesJobCode(self.rapidApp.prefs.video_rename) or rn.usesJobCode(self.rapidApp.prefs.video_subfolder) - if apply: - if overwrite: - self.liststore.set(iter, 8, job_code) - mediaFile.jobcode = job_code - mediaFile.sampleStale = True - else: - if not self.get_job_code(iter): - self.liststore.set(iter, 8, job_code) - mediaFile.jobcode = job_code - mediaFile.sampleStale = True - else: - pass - #if they got an existing job code, may as well keep it there in case the user - #reactivates job codes again in their prefs - - if to_all_rows or thread_id is not None: - for iter in self.get_tree_row_iters(): - apply = True - if thread_id is not None: - t = self.get_thread(iter) - apply = t == thread_id - - if apply: - mediaFile = self.get_mediaFile(iter) - _apply_job_code() - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(iter) - else: - for iter in self.get_tree_row_iters(selected_only = True): - mediaFile = self.get_mediaFile(iter) - _apply_job_code() - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(iter) + def pause(self): + self.paused = True + for scan in self.processes(): + run_event = scan[2] + if run_event.is_set(): + run_event.clear() + + def _terminate_process(self, p): + self._send_termination_msg(p) + # The process might be paused: let it run + run_event = p[2] + if not run_event.is_set(): + run_event.set() - def job_code_missing(self, selected_only): + def _send_termination_msg(self, p): + p[1].put(None) + + def terminate_process(self, process_id): """ - Returns True if any of the pending downloads do not have a - job code assigned. - - If selected_only is True, will only check in rows that the - user has selected. + Send a signal to process with matching process_id that it should + immediately terminate """ - - def _job_code_missing(iter): - status = self.get_status(iter) - if status in [STATUS_WARNING, STATUS_NOT_DOWNLOADED]: - is_image = self.get_is_image(iter) - job_code = self.get_job_code(iter) - return needAJobCode.needAJobCode(job_code, is_image) - return False - - self._setUsesJobCode() - needAJobCode = NeedAJobCode(self.rapidApp.prefs) - - v = False - if selected_only: - selection = self.get_selection() - model, pathlist = selection.get_selected_rows() - for path in pathlist: - iter = self.liststore.get_iter(path) - v = _job_code_missing(iter) - if v: - break - else: - iter = self.liststore.get_iter_first() - while iter: - v = _job_code_missing(iter) - if v: - break - iter = self.liststore.iter_next(iter) - return v - + for p in self.processes(): + if p[0].pid == process_id: + if p[0].is_alive(): + self._terminate_process(p) - def _set_download_pending(self, iter, threads): - existing_status = self.get_status(iter) - if existing_status in [STATUS_WARNING, STATUS_NOT_DOWNLOADED]: - self.liststore.set(iter, 11, STATUS_DOWNLOAD_PENDING) - self.liststore.set(iter, 10, self.download_pending_icon) - # this value is in a thread's list of files to download - mediaFile = self.get_mediaFile(iter) - # each thread will see this change in status - mediaFile.status = STATUS_DOWNLOAD_PENDING - thread = self.get_thread(iter) - if thread not in threads: - threads.append(thread) - - def set_status_to_download_pending(self, selected_only, thread_id=None): + def request_termination(self): """ - Sets status of files to be download pending, if they are waiting to be downloaded - if selected_only is true, only applies to selected rows - - If thread_id is not None, then after the statuses have been set, - the thread will be restarted (this is intended for the cases - where this method is called from a thread and auto start is True) + Send a signal to processes that they should immediately terminate + """ + requested = False + for p in self.processes(): + if p[0].is_alive(): + requested = True + self._terminate_process(p) + + return requested + + def terminate_forcefully(self): + """ + Forcefully terminates any running processes. Use with great caution. + No cleanup action is performed. - Returns a list of threads which can be downloaded + As python essential reference (4th edition) says, if the process + 'holds a lock or is involved with interprocess communication, + terminating it might cause a deadlock or corrupted I/O.' """ - threads = [] - if selected_only: - for iter in self.get_tree_row_iters(selected_only = True): - self._set_download_pending(iter, threads) - else: - for iter in self.get_tree_row_iters(): - apply = True - if thread_id is not None: - t = self.get_thread(iter) - apply = t == thread_id - if apply: - self._set_download_pending(iter, threads) - - if thread_id is not None: - # restart the thread - workers[thread_id].startStop() - return threads - - def update_status_post_download(self, treerowref): - path = treerowref.get_path() - if not path: - sys.stderr.write("FIXME: SelectionTreeView treerowref no longer refers to valid row\n") - else: - iter = self.liststore.get_iter(path) - mediaFile = self.get_mediaFile(iter) - status = mediaFile.status - self.liststore.set(iter, 11, status) - self.liststore.set(iter, 10, self.get_status_icon(status)) + for p in self.processes(): + if p[0].is_alive(): + logger.info("Forcefully terminating %s in %s" , p[0].name, + self.__class__.__name__) + p[0].terminate() + - # If this row is currently previewed, then should update the preview - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(iter) + def get_pipe(self, source): + return self._pipes[source] + + def get_no_active_processes(self): + """ + Returns how many processes are currently active, i.e. running + """ + i = 0 + for p in self.processes(): + if p[0].is_alive(): + i += 1 + return i -class SelectionVBox(gtk.VBox): +class ScanManager(TaskManager): + + def __init__(self, results_callback, batch_size, generate_folder, + add_device_function): + TaskManager.__init__(self, results_callback, batch_size) + self.add_device_function = add_device_function + self.generate_folder = generate_folder + + def _initiate_task(self, device, task_results_conn, task_process_conn, + terminate_queue, run_event): + + scan = scan_process.Scan(device.get_path(), self.batch_size, self.generate_folder, + task_process_conn, terminate_queue, run_event) + scan.start() + self._processes.append((scan, terminate_queue, run_event)) + self.add_device_function(scan.pid, device, + # This refers to when a device like a hard drive is having its contents scanned, + # looking for photos or videos. It is visible initially in the progress bar for each device + # (which normally holds "x photos and videos"). + # It maybe displayed only briefly if the contents of the device being scanned is small. + progress_bar_text=_('scanning...')) + + return scan.pid + +class CopyFilesManager(TaskManager): + + def _initiate_task(self, task, task_results_conn, + task_process_conn, terminate_queue, run_event): + photo_download_folder = task[0] + video_download_folder = task[1] + scan_pid = task[2] + files = task[3] + + copy_files = copyfiles.CopyFiles(photo_download_folder, + video_download_folder, + files, + scan_pid, self.batch_size, + task_process_conn, terminate_queue, run_event) + copy_files.start() + self._processes.append((copy_files, terminate_queue, run_event)) + return copy_files.pid + +class ThumbnailManager(TaskManager): + def _initiate_task(self, task, task_results_conn, + task_process_conn, terminate_queue, run_event): + scan_pid = task[0] + files = task[1] + generator = tn.GenerateThumbnails(scan_pid, files, self.batch_size, + task_process_conn, terminate_queue, + run_event) + generator.start() + self._processes.append((generator, terminate_queue, run_event)) + return generator.pid + +class BackupFilesManager(TaskManager): """ - Dialog from which the user can select photos and videos to download + Handles backup processes. This is a little different from other Task + Manager classes in that its pipe is Duplex, and the work done by it + is not pre-assigned when the process is started. """ + def __init__(self, results_callback, batch_size): + TaskManager.__init__(self, results_callback, batch_size) + self.backup_devices_by_path = {} - - def __init__(self, parentApp): - """ - Initialize values for log dialog, but do not display. - """ + def _setup_pipe(self): + return Pipe(duplex=True) - gtk.VBox.__init__(self) - self.parentApp = parentApp + def _send_termination_msg(self, p): + p[1].put(None) + p[3].send((None, None, None, None)) - tiny_screen = TINY_SCREEN - if tiny_screen: - config.max_thumbnail_size = 160 + def _initiate_task(self, task, task_results_conn, task_process_conn, + terminate_queue, run_event): + path = task[0] + name = task[1] + backup_files = backupfile.BackupFiles(path, name, self.batch_size, + task_process_conn, terminate_queue, + run_event) + backup_files.start() + self._processes.append((backup_files, terminate_queue, run_event, + task_results_conn)) - selection_scrolledwindow = gtk.ScrolledWindow() - selection_scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - selection_viewport = gtk.Viewport() + self.backup_devices_by_path[path] = (task_results_conn, backup_files.pid) + return backup_files.pid - self.selection_treeview = SelectionTreeView(self) + def backup_file(self, move_succeeded, rpd_file, path_suffix, + backup_duplicate_overwrite): + for path in self.backup_devices_by_path: + task_results_conn = self.backup_devices_by_path[path][0] + task_results_conn.send((move_succeeded, rpd_file, path_suffix, + backup_duplicate_overwrite)) + + def add_device(self, path, name): + """ + Convenience function to setup adding a backup device + """ + return self.add_task((path, name)) + + def remove_device(self, path): + pid = self.backup_devices_by_path[path][1] + self.terminate_process(pid) + del self.backup_devices_by_path[path] - selection_scrolledwindow.add(self.selection_treeview) - - - # Job code controls - self.add_job_code_combo() - left_pane_vbox = gtk.VBox(spacing = 12) - left_pane_vbox.pack_start(selection_scrolledwindow, True, True) - left_pane_vbox.pack_start(self.job_code_hbox, False, True) - # Window sizes - #selection_scrolledwindow.set_size_request(350, -1) - - - # Preview pane - - # Zoom in and out slider (make the image bigger / smaller) - - # Zoom out (on the left of the slider) - self.zoom_out_eventbox = gtk.EventBox() - self.zoom_out_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK) - self.zoom_out_image = gtk.Image() - self.zoom_out_image.set_from_file(paths.share_dir('glade3/zoom-out.png')) - self.zoom_out_eventbox.add(self.zoom_out_image) - self.zoom_out_eventbox.connect("button_press_event", self.zoom_out_0_callback) - - # Zoom in (on the right of the slider) - self.zoom_in_eventbox = gtk.EventBox() - self.zoom_in_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK) - self.zoom_in_image = gtk.Image() - self.zoom_in_image.set_from_file(paths.share_dir('glade3/zoom-in.png')) - self.zoom_in_eventbox.add(self.zoom_in_image) - self.zoom_in_eventbox.connect("button_press_event", self.zoom_in_100_callback) - - self.slider_adjustment = gtk.Adjustment(value=self.parentApp.prefs.preview_zoom, - lower=config.MIN_THUMBNAIL_SIZE, upper=config.max_thumbnail_size, - step_incr=1.0, page_incr=config.THUMBNAIL_INCREMENT, page_size=0) - self.slider_adjustment.connect("value_changed", self.resize_image_callback) - self.slider_hscale = gtk.HScale(self.slider_adjustment) - self.slider_hscale.set_draw_value(False) # don't display numeric value - self.slider_hscale.set_size_request(config.MIN_THUMBNAIL_SIZE * 2, -1) +class SingleInstanceTaskManager: + """ + Base class to manage single instance processes. Examples are daemon + processes, but also a non-daemon process that has one simple task. + + Core (infrastructure) functionality is implemented in this class. + Derived classes should implemented functionality to actually implement + specific tasks. + """ + def __init__(self, results_callback): + self.results_callback = results_callback + self.task_results_conn, self.task_process_conn = Pipe(duplex=True) - #Preview image - self.base_preview_image = None # large size image used to scale down from - self.preview_image = gtk.Image() + source = self.task_results_conn.fileno() + gobject.io_add_watch(source, gobject.IO_IN, self.task_results) - self.preview_image.set_alignment(0, 0.5) - #leave room for thumbnail shadow - if DROP_SHADOW: - self.cacheDropShadow() - else: - self.shadow_size = 0 - image_size, shadow_size, offset = self._imageAndShadowSize() +class PreviewManager(SingleInstanceTaskManager): + def __init__(self, results_callback): + SingleInstanceTaskManager.__init__(self, results_callback) + self._get_preview = tn.GetPreviewImage(self.task_process_conn) + self._get_preview.start() - self.preview_image.set_size_request(image_size, image_size) + def get_preview(self, unique_id, full_file_name, file_type, size_max): + self.task_results_conn.send((unique_id, full_file_name, file_type, size_max)) - #labels to display file information + def task_results(self, source, condition): + unique_id, preview_full_size, preview_small = self.task_results_conn.recv() + self.results_callback(unique_id, preview_full_size, preview_small) + return True - #Original filename - self.preview_original_name_label = gtk.Label() - self.preview_original_name_label.set_alignment(0, 0.5) - self.preview_original_name_label.set_ellipsize(pango.ELLIPSIZE_END) +class SubfolderFileManager(SingleInstanceTaskManager): + """ + Manages the daemon process that renames files and creates subfolders + """ + def __init__(self, results_callback, sequence_values): + SingleInstanceTaskManager.__init__(self, results_callback) + self._subfolder_file = subfolderfile.SubfolderFile(self.task_process_conn, sequence_values) + self._subfolder_file.start() + logger.debug("SubfolderFile PID: %s", self._subfolder_file.pid) + + def rename_file_and_move_to_subfolder(self, download_succeeded, + download_count, rpd_file): + + self.task_results_conn.send((download_succeeded, download_count, + rpd_file)) + logger.debug("Download count: %s.", download_count) + + + def task_results(self, source, condition): + move_succeeded, rpd_file = self.task_results_conn.recv() + self.results_callback(move_succeeded, rpd_file) + return True +class ResizblePilImage(gtk.DrawingArea): + def __init__(self, bg_color=None): + gtk.DrawingArea.__init__(self) + self.base_image = None + self.bg_color = bg_color + self.connect('expose_event', self.expose) - #Device (where it will be downloaded from) - self.preview_device_expander = gtk.Expander() - self.preview_device_label = gtk.Label() - self.preview_device_label.set_alignment(0, 0.5) - self.preview_device_image = gtk.Image() + def set_image(self, image): + self.base_image = image - self.preview_device_path_label = gtk.Label() - self.preview_device_path_label.set_alignment(0, 0.5) - self.preview_device_path_label.set_ellipsize(pango.ELLIPSIZE_MIDDLE) - self.preview_device_path_label.set_padding(30, 0) - self.preview_device_expander.add(self.preview_device_path_label) + #set up sizes and ratio used for drawing the derived image + self.base_image_w = self.base_image.size[0] + self.base_image_h = self.base_image.size[1] + self.base_image_aspect = float(self.base_image_w) / self.base_image_h - device_hbox = gtk.HBox(False, spacing = 6) - device_hbox.pack_start(self.preview_device_image) - device_hbox.pack_start(self.preview_device_label, True, True) + self.queue_draw() - self.preview_device_expander.set_label_widget(device_hbox) + def expose(self, widget, event): + + cairo_context = self.window.cairo_create() - #Filename that has been generated - self.preview_name_label = gtk.Label() - self.preview_name_label.set_alignment(0, 0.5) - self.preview_name_label.set_ellipsize(pango.ELLIPSIZE_END) + x = event.area.x + y = event.area.y + w = event.area.width + h = event.area.height - #Download destination - self.preview_destination_expander = gtk.Expander() - self.preview_destination_label = gtk.Label() - self.preview_destination_label.set_alignment(0, 0.5) - self.preview_destination_image = gtk.Image() + #constrain operations to event area + cairo_context.rectangle(x, y, w, h) + cairo_context.clip_preserve() - self.preview_destination_path_label = gtk.Label() - self.preview_destination_path_label.set_alignment(0, 0.5) - self.preview_destination_path_label.set_ellipsize(pango.ELLIPSIZE_MIDDLE) - self.preview_destination_path_label.set_padding(30, 0) - self.preview_destination_expander.add(self.preview_destination_path_label) + #set background color, if needed + if self.bg_color: + cairo_context.set_source_rgb(*self.bg_color) + cairo_context.fill_preserve() - destination_hbox = gtk.HBox(False, spacing = 6) - destination_hbox.pack_start(self.preview_destination_image) - destination_hbox.pack_start(self.preview_destination_label, True, True) + if not self.base_image: + return False + + frame_aspect = float(w) / h - self.preview_destination_expander.set_label_widget(destination_hbox) + if frame_aspect > self.base_image_aspect: + # Frame is wider than image + height = h + width = int(height * self.base_image_aspect) + else: + # Frame is taller than image + width = w + height = int(width / self.base_image_aspect) + + #resize image + pil_image = self.base_image.copy() + if self.base_image_w < width or self.base_image_h < height: + logger.debug("Upsizing image") + pil_image = tn.upsize_pil(pil_image, (width, height)) + else: + logger.debug("Downsizing image") + tn.downsize_pil(pil_image, (width, height)) + #image width and height + image_w = pil_image.size[0] + image_h = pil_image.size[1] - #Status of the file + #center the image horizontally and vertically + #top left and right corners for the image: + image_x = x + ((w - image_w) / 2) + image_y = y + ((h - image_h) / 2) - self.preview_status_icon = gtk.Image() - self.preview_status_icon.set_size_request(16,16) + image = create_cairo_image_surface(pil_image, image_w, image_h) + cairo_context.set_source_surface(image, image_x, image_y) + cairo_context.paint() - self.preview_status_label = gtk.Label() - self.preview_status_label.set_alignment(0, 0.5) - self.preview_status_label.set_ellipsize(pango.ELLIPSIZE_END) - self.preview_status_label.set_padding(12, 0) - - #Title of problems encountered in generating the name / subfolder - self.preview_problem_title_label = gtk.Label() - self.preview_problem_title_label.set_alignment(0, 0.5) - self.preview_problem_title_label.set_ellipsize(pango.ELLIPSIZE_END) - self.preview_problem_title_label.set_padding(12, 0) + return False + - #Details of what the problem(s) are - self.preview_problem_label = gtk.Label() - self.preview_problem_label.set_alignment(0, 0) - self.preview_problem_label.set_line_wrap(True) - self.preview_problem_label.set_padding(12, 0) - #Can't combine wrapping and ellipsize, sadly - #self.preview_problem_label.set_ellipsize(pango.ELLIPSIZE_END) - - #Put content into table - # Use a table so we can do the Gnome HIG layout more easily - self.preview_table = gtk.Table(10, 4) - self.preview_table.set_row_spacings(12) - left_spacer = gtk.Label('') - left_spacer.set_padding(12, 0) - right_spacer = gtk.Label('') - right_spacer.set_padding(6, 0) - - - spacer2 = gtk.Label('') - - #left and right spacers - self.preview_table.attach(left_spacer, 0, 1, 1, 2, xoptions=gtk.SHRINK, yoptions=gtk.SHRINK) - self.preview_table.attach(right_spacer, 3, 4, 1, 2, xoptions=gtk.SHRINK, yoptions=gtk.SHRINK) - - row = 0 - zoom_hbox = gtk.HBox() - zoom_hbox.pack_start(self.zoom_out_eventbox, False, False) - zoom_hbox.pack_start(self.slider_hscale, False, False) - zoom_hbox.pack_start(self.zoom_in_eventbox, False, False) - - self.preview_table.attach(zoom_hbox, 1, 3, row, row+1, yoptions=gtk.SHRINK) - - row += 1 - self.preview_table.attach(self.preview_image, 1, 3, row, row+1, yoptions=gtk.SHRINK) - row += 1 - - self.preview_table.attach(self.preview_original_name_label, 1, 3, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.SHRINK) - row += 1 - if not tiny_screen: - self.preview_table.attach(self.preview_device_expander, 1, 3, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.SHRINK) - row += 1 - - self.preview_table.attach(self.preview_name_label, 1, 3, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.SHRINK) - row += 1 - if not tiny_screen: - self.preview_table.attach(self.preview_destination_expander, 1, 3, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.SHRINK) - row += 1 - - if not tiny_screen: - self.preview_table.attach(spacer2, 0, 7, row, row+1, yoptions=gtk.SHRINK) - row += 1 - - self.preview_table.attach(self.preview_status_icon, 1, 2, row, row+1, xoptions=gtk.SHRINK, yoptions=gtk.SHRINK) - self.preview_table.attach(self.preview_status_label, 2, 3, row, row+1, yoptions=gtk.SHRINK) - row += 1 - - self.preview_table.attach(self.preview_problem_title_label, 2, 3, row, row+1, yoptions=gtk.SHRINK) - row += 1 - self.preview_table.attach(self.preview_problem_label, 2, 4, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.EXPAND|gtk.FILL) - row += 1 - - self.file_hpaned = gtk.HPaned() - self.file_hpaned.pack1(left_pane_vbox, shrink=False) - self.file_hpaned.pack2(self.preview_table, resize=True, shrink=False) - self.pack_start(self.file_hpaned, True, True) - if self.parentApp.prefs.hpaned_pos > 0: - self.file_hpaned.set_position(self.parentApp.prefs.hpaned_pos) - else: - # this is what the user will see the first time they run the app - self.file_hpaned.set_position(300) - self.show_all() - +class PreviewImage: - def set_base_preview_image(self, pixbuf): - """ - sets the unscaled pixbuf image to be displayed to the user - the actual image the user will see will depend on the scale - they've set to view it at - """ - self.base_preview_image = pixbuf + def __init__(self, parent_app, builder): + #set background color to equivalent of '#444444 + self.preview_image = ResizblePilImage(bg_color=(0.267, 0.267, 0.267)) + self.preview_image_eventbox = builder.get_object("preview_eventbox") + self.preview_image_eventbox.add(self.preview_image) + self.preview_image.show() + self.download_this_checkbutton = builder.get_object("download_this_checkbutton") + self.rapid_app = parent_app - def zoom_in(self): - self.slider_adjustment.set_value(min([config.max_thumbnail_size, int(self.slider_adjustment.get_value()) + config.THUMBNAIL_INCREMENT])) + self.base_preview_image = None # large size image used to scale down from + self.current_preview_size = (0,0) + self.preview_image_size_limit = (0,0) - def zoom_out(self): - self.slider_adjustment.set_value(max([config.MIN_THUMBNAIL_SIZE, int(self.slider_adjustment.get_value()) - config.THUMBNAIL_INCREMENT])) - - def zoom_in_100_callback(self, widget, value): - self.slider_adjustment.set_value(config.max_thumbnail_size) + self.unique_id = None - def zoom_out_0_callback(self, widget, value): - self.slider_adjustment.set_value(config.MIN_THUMBNAIL_SIZE) - - def set_display_preview_folders(self, value): - if value and self.selection_treeview.previewed_file_treerowref: - self.preview_destination_expander.show() - self.preview_device_expander.show() - - else: - self.preview_destination_expander.hide() - self.preview_device_expander.hide() - - def cacheDropShadow(self): - i, self.shadow_size, offset_v = self._imageAndShadowSize() - self.drop_shadow = DropShadow(offset=(offset_v,offset_v), shadow = (0x44, 0x44, 0x44, 0xff), border=self.shadow_size, trim_border = True) - - def _imageAndShadowSize(self): - image_size = int(self.slider_adjustment.get_value()) - offset_v = max([image_size / 25, 5]) # realistically size the shadow based on the size of the image - shadow_size = offset_v + 3 - image_size = image_size + offset_v * 2 + 3 - return (image_size, shadow_size, offset_v) - - def resize_image_callback(self, adjustment): - """ - Resize the preview image after the adjustment value has been - changed - """ - size = int(adjustment.value) - self.parentApp.prefs.preview_zoom = size - self.cacheDropShadow() - - pixbuf = self.scaledPreviewImage() - if pixbuf: - self.preview_image.set_from_pixbuf(pixbuf) - size = max([pixbuf.get_width(), pixbuf.get_height()]) - self.preview_image.set_size_request(size, size) - else: - self.preview_image.set_size_request(size + self.shadow_size, size + self.shadow_size) - - def scaledPreviewImage(self): + def set_preview_image(self, unique_id, pil_image, include_checkbutton_visible=None, + checked=None): """ - Generate a scaled version of the preview image """ - size = int(self.slider_adjustment.get_value()) - if not self.base_preview_image: - return None - else: - pixbuf = common.scale2pixbuf(size, size, self.base_preview_image) - - if DROP_SHADOW: - pil_image = pixbuf_to_image(pixbuf) - pil_image = self.drop_shadow.dropShadow(pil_image) - pixbuf = image_to_pixbuf(pil_image) - - return pixbuf - - def set_job_code_display(self): - """ - Shows or hides the job code entry + self.preview_image.set_image(pil_image) + self.unique_id = unique_id + if checked is not None: + self.download_this_checkbutton.set_active(checked) + self.download_this_checkbutton.grab_focus() + + if include_checkbutton_visible is not None: + self.download_this_checkbutton.props.visible = include_checkbutton_visible - If user is not using job codes in their file or subfolder names - then do not prompt for it - """ + def update_preview_image(self, unique_id, pil_image): + if unique_id == self.unique_id: + self.set_preview_image(unique_id, pil_image) + - if self.parentApp.needJobCodeForRenaming(): - self.job_code_hbox.show() - self.job_code_label.show() - self.job_code_combo.show() - self.selection_treeview.job_code_column.set_visible(True) - else: - self.job_code_hbox.hide() - self.job_code_label.hide() - self.job_code_combo.hide() - self.selection_treeview.job_code_column.set_visible(False) - - def update_job_code_combo(self): - # delete existing rows - while len(self.job_code_combo.get_model()) > 0: - self.job_code_combo.remove_text(0) - # add new ones - for text in self.parentApp.prefs.job_codes: - self.job_code_combo.append_text(text) - # clear existing entry displayed in entry box - self.job_code_entry.set_text('') +class RapidApp(dbus.service.Object): + """ + The main Rapid Photo Downloader application class. - def add_job_code_combo(self): - self.job_code_hbox = gtk.HBox(spacing = 12) - self.job_code_hbox.set_no_show_all(True) - self.job_code_label = gtk.Label(_("Job Code:")) - - self.job_code_combo = gtk.combo_box_entry_new_text() - for text in self.parentApp.prefs.job_codes: - self.job_code_combo.append_text(text) - - # make entry box have entry completion - self.job_code_entry = self.job_code_combo.child - - self.completion = gtk.EntryCompletion() - self.completion.set_match_func(self.job_code_match_func) - self.completion.connect("match-selected", - self.on_job_code_combo_completion_match) - self.completion.set_model(self.job_code_combo.get_model()) - self.completion.set_text_column(0) - self.job_code_entry.set_completion(self.completion) - - - self.job_code_combo.connect('changed', self.on_job_code_resp) - - self.job_code_entry.connect('activate', self.on_job_code_entry_resp) - - self.job_code_combo.set_tooltip_text(_("Enter a new Job Code and press Enter, or select an existing Job Code")) - - #add widgets - self.job_code_hbox.pack_start(self.job_code_label, False, False) - self.job_code_hbox.pack_start(self.job_code_combo, True, True) - self.set_job_code_display() - - def job_code_match_func(self, completion, key, iter): - model = completion.get_model() - return model[iter][0].lower().startswith(self.job_code_entry.get_text().lower()) - - def on_job_code_combo_completion_match(self, completion, model, iter): - self.job_code_entry.set_text(model[iter][0]) - self.job_code_entry.set_position(-1) - - def on_job_code_resp(self, widget): - """ - When the user has clicked on an existing job code - """ + Contains functionality for main program window, and directs all other + processes. + """ + + def __init__(self, bus, path, name, taskserver=None): - # ignore changes because the user is typing in a new value - if widget.get_active() >= 0: - self.job_code_chosen(widget.get_active_text()) - - def on_job_code_entry_resp(self, widget): - """ - When the user has hit enter after entering a new job code - """ - self.job_code_chosen(widget.get_text()) + dbus.service.Object.__init__ (self, bus, path, name) + self.running = False - def job_code_chosen(self, job_code): - """ - The user has selected a Job code, apply it to selected images. - """ - self.selection_treeview.apply_job_code(job_code, overwrite = True) - self.completion.set_model(None) - self.parentApp.assignJobCode(job_code) - self.completion.set_model(self.job_code_combo.get_model()) - - def add_file(self, mediaFile): - self.selection_treeview.add_file(mediaFile) - + self.taskserver = taskserver -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. - """ + # Setup program preferences, and set callback for when they change + self._init_prefs() - gnomeglade.Component.__init__(self, - paths.share_dir(config.GLADE_FILE), - "logdialog") - + # Initialize widgets in the main window, and variables that point to them + self._init_widgets() + self._init_pynotify() - self.widget.connect("delete-event", self.hide_window) + # Initialize job code handling + self._init_job_code() - self.parentApp = parentApp - self.log_textview.set_cursor_visible(False) - self.textbuffer = self.log_textview.get_buffer() + # Remember the window size from the last time the program was run, or + # set a default size + self._set_window_size() - self.errorTag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD, foreground="red") - self.warningTag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD) - self.resolutionTag = self.textbuffer.create_tag(style=pango.STYLE_ITALIC) + # Setup various widgets + self._setup_buttons() + self._setup_error_icons() + self._setup_icons() + + # Show the main window + self.rapidapp.show() - 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() + # Check program preferences - don't allow auto start if there is a problem + prefs_valid, msg = prefsrapid.check_prefs_for_validity(self.prefs) + if not prefs_valid: + self.notify_prefs_are_invalid(details=msg) - iter = self.textbuffer.get_end_iter() - if severity in [config.CRITICAL_ERROR, config.SERIOUS_ERROR]: - self.textbuffer.insert_with_tags(iter, problem +"\n", self.errorTag) - else: - self.textbuffer.insert_with_tags(iter, problem +"\n", self.warningTag) - 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 + # Initialize variables with which to track important downloads results + self._init_download_tracking() + + # Set up process managers. + # A task such as scanning a device or copying files is handled in its + # own process. + self._start_process_managers() + + # Setup devices from which to download from and backup to + self.setup_devices(on_startup=True, on_preference_change=False, + block_auto_start=not prefs_valid) + + # Ensure the device collection scrolled window is not too small + self._set_device_collection_size() + + def on_rapidapp_destroy(self, widget, data=None): + self._terminate_processes(terminate_file_copies = True) + # save window and component sizes + self.prefs.vpaned_pos = self.main_vpaned.get_position() -class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): - def __init__(self, bus, path, name): + x, y, width, height = self.rapidapp.get_allocation() + self.prefs.main_window_size_x = width + self.prefs.main_window_size_y = height - dbus.service.Object.__init__ (self, bus, path, name) - self.running = False + self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker) - gladefile = paths.share_dir(config.GLADE_FILE) - - gnomeglade.GnomeApp.__init__(self, "rapid", __version__, gladefile, "rapidapp") - - # notifications - self.displayDownloadSummaryNotification = False - self.initPyNotify() + gtk.main_quit() - self.prefs = RapidPreferences() - self.prefs.notify_add(self.on_preference_changed) + def _terminate_processes(self, terminate_file_copies=False): - self.testing = False - if self.testing: - self.setTestingEnv() - -# sys.exit(0) + if terminate_file_copies: + logger.info("Terminating all processes...") - # remember the window size from the last time the program was run - if self.prefs.main_window_maximized: - self.rapidapp.maximize() - elif self.prefs.main_window_size_x > 0: - self.rapidapp.set_default_size(self.prefs.main_window_size_x, self.prefs.main_window_size_y) + scan_termination_requested = self.scan_manager.request_termination() + thumbnails_termination_requested = self.thumbnails.thumbnail_manager.request_termination() + backup_termination_requested = self.backup_manager.request_termination() + + if terminate_file_copies: + copy_files_termination_requested = self.copy_files_manager.request_termination() else: - # set a default size - self.rapidapp.set_default_size(650, 650) - - if gtk.gdk.screen_height() <= config.TINY_SCREEN_HEIGHT: - self.prefs.display_preview_folders = False - self.menu_preview_folders.set_sensitive(False) - - self.widget.show() + copy_files_termination_requested = False - self._setupIcons() + if (scan_termination_requested or thumbnails_termination_requested or + backup_termination_requested): + time.sleep(1) + if (self.scan_manager.get_no_active_processes() > 0 or + self.thumbnails.thumbnail_manager.get_no_active_processes() > 0 or + self.backup_manager.get_no_active_processes() > 0): + time.sleep(1) + # must try again, just in case a new scan has meanwhile started! + self.scan_manager.request_termination() + self.thumbnails.thumbnail_manager.terminate_forcefully() + self.scan_manager.terminate_forcefully() + self.backup_manager.terminate_forcefully() + + if terminate_file_copies and copy_files_termination_requested: + time.sleep(1) + self.copy_files_manager.terminate_forcefully() - # this must come after the window is shown - if self.prefs.vpaned_pos > 0: - self.main_vpaned.set_position(self.prefs.vpaned_pos) - else: - self.main_vpaned.set_position(66) + if terminate_file_copies: + self._clean_all_temp_dirs() - self.checkIfFirstTimeProgramEverRun() + # # # + # Events and tasks related to displaying preview images and thumbnails + # # # - displayPreferences = self.checkForUpgrade(__version__) - self.prefs.program_version = __version__ + def on_download_this_checkbutton_toggled(self, checkbutton): + value = checkbutton.get_active() + self.thumbnails.set_selected(self.preview_image.unique_id, value) + self.set_download_action_sensitivity() + + def on_preview_eventbox_button_press_event(self, widget, event): - self.timeRemaining = TimeRemaining() - self._resetDownloadInfo() - self.statusbar_context_id = self.rapid_statusbar.get_context_id("progress") + if event.type == gtk.gdk._2BUTTON_PRESS and event.button == 1: + self.show_thumbnails() + + def on_show_thumbnails_action_activate(self, action): + logger.debug("on_show_thumbnails_action_activate") + self.show_thumbnails() - # 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() + def on_show_image_action_activate(self, action): + logger.debug("on_show_image_action_activate") + self.thumbnails.show_preview() - if not displayPreferences: - displayPreferences = not self.checkPreferencesOnStartup() + def on_check_all_action_activate(self, action): + self.thumbnails.check_all(check_all=True) - # display download information using threads - global media_collection_treeview, log_dialog - global workers + def on_uncheck_all_action_activate(self, action): + self.thumbnails.check_all(check_all=False) - #track files that should have a suffix added to them - global duplicate_files + def on_check_all_photos_action_activate(self, action): + self.thumbnails.check_all(check_all=True, + file_type=rpdfile.FILE_TYPE_PHOTO) - #track files that have been downloaded in this session - global downloaded_files + def on_check_all_videos_action_activate(self, action): + self.thumbnails.check_all(check_all=True, + file_type=rpdfile.FILE_TYPE_VIDEO) + + def on_quit_action_activate(self, action): + self.on_rapidapp_destroy(widget=self.rapidapp, data=None) - # control sequence numbers and letters - global sequences + def on_refresh_action_activate(self, action): + self.setup_devices(on_startup=False, on_preference_change=False, + block_auto_start=True) + + def on_get_help_action_activate(self, action): + webbrowser.open("http://www.damonlynch.net/rapid/help.html") - # whether we need to prompt for a job code - global need_job_code_for_renaming - - duplicate_files = {} - downloaded_files = DownloadedFiles() + def on_about_action_activate(self, action): + self.about.set_property("name", PROGRAM_NAME) + self.about.set_property("version", utilities.human_readable_version( + __version__)) + self.about.run() + self.about.destroy() + + def on_report_problem_action_activate(self, action): + webbrowser.open("https://bugs.launchpad.net/rapid") + + def on_translate_action_activate(self, action): + webbrowser.open("http://www.damonlynch.net/rapid/translate.html") + + def on_donate_action_activate(self, action): + webbrowser.open("http://www.damonlynch.net/rapid/donate.html") + + def show_preview_image(self, unique_id, image, include_checkbutton_visible, checked): + if self.main_notebook.get_current_page() == 0: # thumbnails + logger.debug("Switching to preview image display") + self.main_notebook.set_current_page(1) + self.preview_image.set_preview_image(unique_id, image, include_checkbutton_visible, checked) + self.next_image_action.set_sensitive(True) + self.prev_image_action.set_sensitive(True) - self.download_start_time = None + def update_preview_image(self, unique_id, image): + self.preview_image.update_preview_image(unique_id, image) - downloadsToday = self.prefs.getAndMaybeResetDownloadsToday() - sequences = rn.Sequences(downloadsToday, self.prefs.stored_sequence_no) + def show_thumbnails(self): + logger.debug("Switching to thumbnails display") + self.main_notebook.set_current_page(0) + self.thumbnails.select_image(self.preview_image.unique_id) + self.next_image_action.set_sensitive(False) + self.prev_image_action.set_sensitive(False) - 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() - self.downloadedFilesLock = Lock() - - # log window, in dialog format - # used for displaying download information to the user + def on_next_image_action_activate(self, action): + if self.preview_image.unique_id is not None: + self.thumbnails.show_next_image(self.preview_image.unique_id) + + def on_prev_image_action_activate(self, action): + if self.preview_image.unique_id is not None: + self.thumbnails.show_prev_image(self.preview_image.unique_id) - log_dialog = LogDialog(self) + def set_thumbnail_sort(self): + """ + If all the scans are complete, sets the sort order + """ + if self.scan_manager.get_no_active_processes() == 0: + self.thumbnails.sort_by_timestamp() - self.volumeMonitor = None - if self.usingVolumeMonitor(): - self.startVolumeMonitor() + # # # + # Volume management + # # # + + def start_volume_monitor(self): + if not self.vmonitor: + self.vmonitor = gio.volume_monitor_get() + self.vmonitor.connect("mount-added", self.on_mount_added) + self.vmonitor.connect("mount-removed", self.on_mount_removed) + + + def _backup_device_name(self, path): + if self.backup_devices[path] is None: + name = path + else: + name = self.backup_devices[path].get_name() + return name - # flag to indicate whether the user changed some preferences that - # indicate the image and backup devices should be setup again - self.rerunSetupAvailableImageAndVideoMedia = False - self.rerunSetupAvailableBackupMedia = False + def setup_devices(self, on_startup, on_preference_change, block_auto_start): + """ - # flag to indicate the user changes some preferences and the display - # of sample names and subfolders needs to be refreshed - self.refreshGeneratedSampleSubfolderAndName = False + Setup devices from which to download from and backup to - # counter to indicate how many threads need their sample names and subfolders regenerated because the user - # changes their prefs at the same time as devices were being scanned - self.noAfterScanRefreshGeneratedSampleSubfolderAndName = 0 + Sets up volumes for downloading from and backing up to - # flag to indicate the user changes some preferences and the display - # of sample download folders needs to be refreshed - self.refreshSampleDownloadFolder = False - self.noAfterScanRefreshSampleDownloadFolders = 0 + on_startup should be True if the program is still starting, + i.e. this is being called from the program's initialization. - # flag to indicate that the preferences dialog window is being - # displayed to the user - self.preferencesDialogDisplayed = False + on_preference_change should be True if this is being called as the + result of a preference being changed - # 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) + block_auto_start should be True if automation options to automatically + start a download should be ignored - #Selection display - self.selection_vbox = SelectionVBox(self) - self.selection_hbox.pack_start(self.selection_vbox, padding=12) - self.set_display_selection(self.prefs.display_selection) - self.set_display_preview_folders(self.prefs.display_preview_folders) + Removes any image media that are currently not downloaded, + or finished downloading + """ + + if self.using_volume_monitor(): + self.start_volume_monitor() - self.backupVolumes = {} - #Help button and download buttons - self._setupDownloadbuttons() + self.clear_non_running_downloads() - #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) + mounts = [] + self.backup_devices = {} + if self.using_volume_monitor(): + # either using automatically detected backup devices + # or download devices + for mount in self.vmonitor.get_mounts(): + if not mount.is_shadowed(): + path = mount.get_root().get_path() + if path: + if (path in self.prefs.device_blacklist and + self.search_for_PSD()): + logger.info("%s ignored", mount.get_name()) + else: + logger.info("Detected %s", mount.get_name()) + is_backup_mount = self.check_if_backup_mount(path) + if is_backup_mount: + self.backup_devices[path] = mount + elif (self.prefs.device_autodetection and + (dv.is_DCIM_device(path) or + self.search_for_PSD())): + mounts.append((path, mount)) + + + if not self.prefs.device_autodetection: + # user manually specified the path from which to download + path = self.prefs.device_location + if path: + logger.info("Using manually specified path %s", path) + if utilities.is_directory(path): + mounts.append((path, None)) + else: + logger.error("Download path does not exist: %s", path) - # menus - - #preview panes - self.menu_display_selection.set_active(self.prefs.display_selection) - self.menu_preview_folders.set_active(self.prefs.display_preview_folders) + if self.prefs.backup_images: + if not self.prefs.backup_device_autodetection: + # user manually specified backup location + # will backup to this path, but don't need any volume info + # associated with it + self.backup_devices[self.prefs.backup_location] = None + + for path in self.backup_devices: + name = self._backup_device_name(path) + self.backup_manager.add_device(path, name) + + self.update_no_backup_devices() - #preview columns in pane - if not DOWNLOAD_VIDEO: - self.menu_type_column.set_active(False) - self.menu_type_column.set_sensitive(False) + # Display amount of free space in a status bar message + self.display_free_space() + + if block_auto_start: + self.auto_start_is_on = False else: - self.menu_type_column.set_active(self.prefs.display_type_column) - self.menu_size_column.set_active(self.prefs.display_size_column) - self.menu_filename_column.set_active(self.prefs.display_filename_column) - self.menu_device_column.set_active(self.prefs.display_device_column) - self.menu_path_column.set_active(self.prefs.display_path_column) + self.auto_start_is_on = ((not on_preference_change) and + ((self.prefs.auto_download_at_startup and + on_startup) or + (self.prefs.auto_download_upon_device_insertion and + not on_startup))) - self.menu_clear.set_sensitive(False) - - need_job_code_for_renaming = self.needJobCodeForRenaming() - self.menu_select_all_without_job_code.set_sensitive(need_job_code_for_renaming) - self.menu_select_all_with_job_code.set_sensitive(need_job_code_for_renaming) + for m in mounts: + path, mount = m + device = dv.Device(path=path, mount=mount) + if (self.search_for_PSD() and + path not in self.prefs.device_whitelist): + # prompt user to see if device should be used or not + self.get_use_device(device) + else: + scan_pid = self.scan_manager.add_task(device) + if mount is not None: + self.mounts_by_path[path] = scan_pid + if not mounts: + self.set_download_action_sensitivity() - #job code initialization - self.last_chosen_job_code = None - self.prompting_for_job_code = False + def get_use_device(self, device): + """ Prompt user whether or not to download from this device """ + + logger.info("Prompting whether to use %s", device.get_name()) + d = dv.UseDeviceDialog(self.rapidapp, device, self.got_use_device) + + def got_use_device(self, dialog, user_selected, permanent_choice, device): + """ User has chosen whether or not to use a device to download from """ + dialog.destroy() - #check to see if the download folder exists and is writable - displayPreferences_2 = not self.checkDownloadPathOnStartup() - displayPreferences = displayPreferences or displayPreferences_2 + path = device.get_path() + + if user_selected: + if permanent_choice and path not in self.prefs.device_whitelist: + # do NOT do a list append operation here without the assignment, + # or the actual preferences will not be updated!! + if len(self.prefs.device_whitelist): + self.prefs.device_whitelist = self.prefs.device_whitelist + [path] + else: + self.prefs.device_whitelist = [path] + scan_pid = self.scan_manager.add_task(device) + self.mounts_by_path[path] = scan_pid - if self.prefs.device_autodetection == False: - displayPreferences_2 = not self.checkImageDevicePathOnStartup() - displayPreferences = displayPreferences or displayPreferences_2 + elif permanent_choice and path not in self.prefs.device_blacklist: + # do not do a list append operation here without the assignment, or the preferences will not be updated! + if len(self.prefs.device_blacklist): + self.prefs.device_blacklist = self.prefs.device_blacklist + [path] + else: + self.prefs.device_blacklist = [path] + + def search_for_PSD(self): + """ + Check to see if user preferences are to automatically search for + Portable Storage Devices or not + """ + return self.prefs.device_autodetection_psd and self.prefs.device_autodetection + + def check_if_backup_mount(self, path): + """ + Checks to see if backups are enabled and path represents a valid backup location - #setup download and backup mediums, initiating scans - self.setupAvailableImageAndBackupMedia(onStartup=True, onPreferenceChange=False, doNotAllowAutoStart = displayPreferences) + Checks against user preferences. + """ + identifiers = [self.prefs.backup_identifier] + if DOWNLOAD_VIDEO: + identifiers.append(self.prefs.video_backup_identifier) + if self.prefs.backup_images: + if self.prefs.backup_device_autodetection: + if dv.is_backup_media(path, identifiers): + return True + elif path == self.prefs.backup_location: + # user manually specified the path + return True + return False + + def update_no_backup_devices(self): + self.download_tracker.set_no_backup_devices(len(self.backup_devices)) - #adjust viewport size for displaying media - #this is important because the code in MediaTreeView.addCard() is inaccurate at program startup + def refresh_backup_media(self): + """ + Setup the backup media - if media_collection_treeview.mapThreadToRow: - height = self.media_collection_viewport.size_request()[1] - self.media_collection_scrolledwindow.set_size_request(-1, height) - else: - # don't allow the media collection to be absolutely empty - self.media_collection_scrolledwindow.set_size_request(-1, 47) + Assumptions: this is being called after the user has changed their + preferences AND download media has already been setup + """ - self.download_button.grab_default() - # for some reason, the grab focus command is not working... unsure why - self.download_button.grab_focus() + # terminate any running backup processes + self.backup_manager.request_termination() - if displayPreferences: - PreferencesDialog(self) + self.backup_devices = {} + if self.prefs.backup_images: + if not self.prefs.backup_device_autodetection: + # user manually specified backup location + # will backup to this path, but don't need any volume info associated with it + self.backup_devices[self.prefs.backup_location] = None + else: + for mount in self.vmonitor.get_mounts(): + if not mount.is_shadowed(): + path = mount.get_root().get_path() + if path: + if self.check_if_backup_mount(path): + # is a backup volume + if path not in self.backup_devices: + self.backup_devices[path] = mount + for path in self.backup_devices: + name = self._backup_device_name(path) + self.backup_manager.add_device(path, name) - - @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 -# if not using_gio: - self.main() -# else: -# mainloop = gobject.MainLoop() -# mainloop.run() - self.running = False + self.update_no_backup_devices() + self.display_free_space() - def setTestingEnv(self): - #self.prefs.program_version = '0.0.8~b7' - p = ['Date time', 'Image date', 'YYYYMMDD', 'Text', '-', '', 'Date time', 'Image date', 'HHMM', 'Text', '-', '', rn.SEQUENCES, rn.DOWNLOAD_SEQ_NUMBER, rn.SEQUENCE_NUMBER_3, 'Text', '-iso', '', 'Metadata', 'ISO', '', 'Text', '-f', '', 'Metadata', 'Aperture', '', 'Text', '-', '', 'Metadata', 'Focal length', '', 'Text', 'mm-', '', 'Metadata', 'Exposure time', '', 'Filename', 'Extension', 'lowercase'] - v = ['Date time', 'Video date', 'YYYYMMDD', 'Text', '-', '', 'Date time', 'Video date', 'HHMM', 'Text', '-', '', 'Sequences', 'Downloads today', 'One digit', 'Text', '-', '', 'Metadata', 'Width', '', 'Text', 'x', '', 'Metadata', 'Height', '', 'Filename', 'Extension', 'lowercase'] - f = '/home/damon/store/rapid-dump' - self.prefs.image_rename = p - self.prefs.video_rename = v - self.prefs.download_folder = f - self.prefs.video_download_folder = f - - - def _setupIcons(self): - icons = ['rapid-photo-downloader-downloaded', - 'rapid-photo-downloader-downloaded-with-error', - 'rapid-photo-downloader-downloaded-with-warning', - 'rapid-photo-downloader-download-pending', - 'rapid-photo-downloader-jobcode'] - - icon_list = [(icon, paths.share_dir('glade3/%s.svg' % icon)) for icon in icons] - common.register_iconsets(icon_list) - - def displayFreeSpace(self): + def using_volume_monitor(self): """ - Displays the amount of space free on the filesystem the files will be downloaded to. - Also displays backup volumes / path being used. + Returns True if programs needs to use gio volume monitor """ - msg = '' - if using_gio and os.path.isdir(self.prefs.download_folder): - folder = gio.File(self.prefs.download_folder) - fileInfo = folder.query_filesystem_info(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE) - free = common.formatSizeForUser(fileInfo.get_attribute_uint64(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE)) - msg = " " + _("%(free)s available") % {'free': free} + return (self.prefs.device_autodetection or + (self.prefs.backup_images and + self.prefs.backup_device_autodetection + )) + + def on_mount_added(self, vmonitor, mount): + """ + callback run when gio indicates a new volume + has been mounted + """ + + + if mount.is_shadowed(): + # ignore this type of mount + return - if self.prefs.backup_images: - if not self.prefs.backup_device_autodetection: - # user manually specified backup location - msg2 = _('Backing up to %(path)s') % {'path':self.prefs.backup_location} + path = mount.get_root().get_path() + if path is not None: + + if path in self.prefs.device_blacklist and self.search_for_PSD(): + logger.info("Device %(device)s (%(path)s) ignored" % { + 'device': mount.get_name(), 'path': path}) else: - msg2 = self.displayBackupVolumes() + is_backup_mount = self.check_if_backup_mount(path) + + if is_backup_mount: + if path not in self.backup_devices: + self.backup_devices[path] = mount + name = self._backup_device_name(path) + self.backup_manager.add_device(path, name) + self.update_no_backup_devices() + self.display_free_space() + + elif self.prefs.device_autodetection and (dv.is_DCIM_device(path) or + self.search_for_PSD()): + + self.auto_start_is_on = self.prefs.auto_download_upon_device_insertion + device = dv.Device(path=path, mount=mount) + if self.search_for_PSD() and path not in self.prefs.device_whitelist: + # prompt user if device should be used or not + self.get_use_device(device) + else: + scan_pid = self.scan_manager.add_task(device) + self.mounts_by_path[path] = scan_pid + + def on_mount_removed(self, vmonitor, mount): + """ + callback run when gio indicates a new volume + has been mounted + """ + + path = mount.get_root().get_path() + + # three scenarios - + # the mount has been scanned but downloading has not yet started + # files are being downloaded from mount (it must be a messy unmount) + # files have finished downloading from mount + + if path in self.mounts_by_path: + scan_pid = self.mounts_by_path[path] + del self.mounts_by_path[path] + # temp directory should be cleaned by finishing of process + + self.thumbnails.clear_all(scan_pid = scan_pid, + keep_downloaded_files = True) + self.device_collection.remove_device(scan_pid) + - if msg: - msg = _("%(freespace)s. %(backuppaths)s.") % {'freespace': msg, 'backuppaths': msg2} - else: - msg = msg2 + + # remove backup volumes + elif path in self.backup_devices: + del self.backup_devices[path] + self.display_free_space() + self.backup_manager.remove_device(path) + self.update_no_backup_devices() + + # may need to disable download button and menu + self.set_download_action_sensitivity() - self.rapid_statusbar.push(self.statusbar_context_id, msg) - - def checkImageDevicePathOnStartup(self): - msg = None - if not os.path.isdir(self.prefs.device_location): - msg = _("Sorry, this device location does not exist:\n%(path)s\n\nPlease resolve the problem, or modify your preferences." % {"path": self.prefs.device_location}) + def clear_non_running_downloads(self): + """ + Clears the display of downloads that are currently not running + """ + + # Stop any processes currently scanning or creating thumbnails + self._terminate_processes(terminate_file_copies=False) + + # Remove them from the user interface + for scan_pid in self.device_collection.get_all_displayed_processes(): + if scan_pid not in self.download_active_by_scan_pid: + self.device_collection.remove_device(scan_pid) + self.thumbnails.clear_all(scan_pid=scan_pid) - if msg: - sys.stderr.write(msg +'\n') - misc.run_dialog(_("Problem with Device Location Folder"), msg, - self, - gtk.MESSAGE_ERROR) - return False - else: - return True - def checkDownloadPathOnStartup(self): - if DOWNLOAD_VIDEO: - paths = ((self.prefs.download_folder, _('Photo')), (self.prefs.video_download_folder, _('Video'))) + + + # # # + # Download and help buttons, and menu items + # # # + + def on_download_action_activate(self, action): + """ + Called when a download is activated + """ + + if self.copy_files_manager.paused: + logger.debug("Download resumed") + self.resume_download() else: - paths = ((self.prefs.download_folder, _('Photo')),) - msg = '' - noProblems = 0 - for path, file_type in paths: - if not os.path.isdir(path): - msg += _("The %(file_type)s Download Folder does not exist.\n") % {'file_type': file_type} - noProblems += 1 - else: - #unfortunately 'os.access(self.prefs.download_folder, os.W_OK)' is not reliable - try: - tempWorkingDir = tempfile.mkdtemp(prefix='rapid-tmp-', - dir=path) - except: - noProblems += 1 - msg += _("The %(file_type)s Download Folder exists but cannot be written to.\n") % {'file_type': file_type} - else: - os.rmdir(tempWorkingDir) + logger.debug("Download activated") - if msg: - msg = _("Sorry, problems were encountered with your download folders. Please fix the problems or modify the preferences.\n\n") + msg - sys.stderr.write(msg) - if noProblems == 1: - title = _("Problem with Download Folder") + if self.download_action_is_download: + if self.need_job_code_for_naming and not self.prompting_for_job_code: + self.get_job_code() + else: + self.start_download() else: - title = _("Problem with Download Folders") - - misc.run_dialog(title, msg, - self, - gtk.MESSAGE_ERROR) - return False - else: - return True + self.pause_download() + - def checkPreferencesOnStartup(self): - prefsOk = rn.checkPreferencesForValidity(self.prefs.image_rename, self.prefs.subfolder, self.prefs.video_rename, self.prefs.video_subfolder) - if not prefsOk: - msg = _("There is an error in the program preferences.") - msg += " " + _("Some preferences will be reset.") - # do not use cmd_line here, as this is a genuine error - sys.stderr.write(msg +'\n') - return prefsOk + def on_help_action_activate(self, action): + webbrowser.open("http://www.damonlynch.net/rapid/documentation") - def needJobCodeForRenaming(self): - return rn.usesJobCode(self.prefs.image_rename) or rn.usesJobCode(self.prefs.subfolder) or rn.usesJobCode(self.prefs.video_rename) or rn.usesJobCode(self.prefs.video_subfolder) + def on_preferences_action_activate(self, action): + + preferencesdialog.PreferencesDialog(self) - def assignJobCode(self, code): - """ assign job code (which may be empty) to global variable and update user preferences + def set_download_action_sensitivity(self): + """ + Sets sensitivity of Download action to enable or disable it + + Affects download button and menu item + """ + if not self.download_is_occurring(): + sensitivity = False + if self.scan_manager.no_tasks == 0: + if self.thumbnails.files_are_checked_to_download(): + sensitivity = True + + self.download_action.set_sensitive(sensitivity) + + def set_download_action_label(self, is_download): + """ + Toggles label betwen pause and download + """ + + if is_download: + self.download_action.set_label(_("Download")) + self.download_action_is_download = True + else: + self.download_action.set_label(_("Pause")) + self.download_action_is_download = False + + # # # + # Job codes + # # # + + + def _init_job_code(self): + self.job_code = self.last_chosen_job_code = '' + self.need_job_code_for_naming = self.prefs.any_pref_uses_job_code() + self.prompting_for_job_code = False + + def assign_job_code(self, code): + """ assign job code (which may be empty) to member variable and update user preferences Update preferences only if code is not empty. Do not duplicate job code. """ - global job_code - if code == None: - code = '' - job_code = code + + self.job_code = code - if job_code: + if code: #add this value to job codes preferences #delete any existing value which is the same #(this way it comes to the front, which is where it should be) @@ -5163,1301 +2001,1272 @@ class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): jcs.remove(code) self.prefs.job_codes = [code] + jcs - - - def getShowWarningDownloadingFromCamera(self): - if self.prefs.show_warning_downloading_from_camera: - cmd_line(_("Displaying warning about downloading directly from camera")) - d = ShowWarningDialog(self.widget, self.gotShowWarningDownloadingFromCamera) - - def gotShowWarningDownloadingFromCamera(self, dialog, showWarningAgain): - dialog.destroy() - self.prefs.show_warning_downloading_from_camera = showWarningAgain - - 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, downloadSelected): + + def _get_job_code(self, post_job_code_entry_callback): """ prompt for a job code """ if not self.prompting_for_job_code: - cmd_line(_("Prompting for Job Code")) + logger.debug("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, downloadSelected, False) + j = preferencesdialog.JobCodeDialog(parent_window = self.rapidapp, + job_codes = self.prefs.job_codes, + default_job_code = self.last_chosen_job_code, + post_job_code_entry_callback=post_job_code_entry_callback, + entry_only = False) else: - cmd_line(_("Already prompting for Job Code, do not prompt again")) - - def getJobCode(self, autoStart=True, downloadSelected=False): - """ called from the copyphotos thread, or when the user clicks one of the two download buttons""" + logger.debug("Already prompting for Job Code, do not prompt again") - self._getJobCode(self.gotJobCode, autoStart, downloadSelected) + def get_job_code(self): + self._get_job_code(self.got_job_code) - def gotJobCode(self, dialog, userChoseCode, code, autoStart, downloadSelected): + def got_job_code(self, dialog, user_chose_code, code): dialog.destroy() self.prompting_for_job_code = False - if userChoseCode: - self.assignJobCode(code) + if user_chose_code: + if code is None: + code = '' + self.assign_job_code(code) self.last_chosen_job_code = code - self.selection_vbox.selection_treeview.apply_job_code(code, overwrite=False, to_all_rows = not downloadSelected) - threads = self.selection_vbox.selection_treeview.set_status_to_download_pending(selected_only = downloadSelected) - if downloadSelected or not autoStart: - cmd_line(_("Starting downloads")) - self.startDownload(threads) - else: - # autostart is true - cmd_line(_("Starting downloads that have been waiting for a Job Code")) - for w in workers.getWaitingForJobCodeWorkers(): - w.startStop() + logger.debug("Job Code %s entered", self.job_code) + self.start_download() else: # user cancelled - for w in workers.getWaitingForJobCodeWorkers(): - w.waitingForJobCode = False - - if autoStart: - for w in workers.getAutoStartWorkers(): - w.autoStart = False - - def addFile(self, mediaFile): - self.selection_vbox.add_file(mediaFile) - - def update_status_post_download(self, treerowref): - self.selection_vbox.selection_treeview.update_status_post_download(treerowref) - - def on_menu_size_column_toggled(self, widget): - self.prefs.display_size_column = widget.get_active() - self.selection_vbox.selection_treeview.display_size_column(self.prefs.display_size_column) - - def on_menu_type_column_toggled(self, widget): - self.prefs.display_type_column = widget.get_active() - self.selection_vbox.selection_treeview.display_type_column(self.prefs.display_type_column) + logger.debug("No Job Code entered") + self.job_code = '' + self.auto_start_is_on = False + + + # # # + # Download + # # # + + def _init_download_tracking(self): + """ + Initialize variables to track downloads + """ + # Track download sizes and other values for each device. + # (Scan id acts as an index to each device. A device could be scanned + # more than once). + self.download_tracker = downloadtracker.DownloadTracker() - def on_menu_filename_column_toggled(self, widget): - self.prefs.display_filename_column = widget.get_active() - self.selection_vbox.selection_treeview.display_filename_column(self.prefs.display_filename_column) + # Track which temporary directories are created when downloading files + self.temp_dirs_by_scan_pid = dict() - def on_menu_path_column_toggled(self, widget): - self.prefs.display_path_column = widget.get_active() - self.selection_vbox.selection_treeview.display_path_column(self.prefs.display_path_column) + # Track which downloads and backups are running + self.download_active_by_scan_pid = [] + self.backups_active_by_scan_pid = [] - def on_menu_device_column_toggled(self, widget): - self.prefs.display_device_column = widget.get_active() - self.selection_vbox.selection_treeview.display_device_column(self.prefs.display_device_column) - - def checkIfFirstTimeProgramEverRun(self): + + + def start_download(self, scan_pid=None): """ - if this is the first time the program has been run, then - might need to create default directories + Start download, renaming and backup of files. + + If scan_pid is specified, only files matching it will be downloaded """ - if len(self.prefs.program_version) == 0: - path = getDefaultPhotoLocation(ignore_missing_dir=True) - if not os.path.isdir(path): - cmd_line(_("Creating photo download folder %(folder)s") % {'folder':path}) - try: - os.makedirs(path) - self.prefs.download_folder = path - except: - cmd_line(_("Failed to create default photo download folder %(folder)s") % {'folder':path}) - if DOWNLOAD_VIDEO: - path = getDefaultVideoLocation(ignore_missing_dir=True) - if not os.path.isdir(path): - cmd_line(_("Creating video download folder %(folder)s") % {'folder':path}) - try: - os.makedirs(path) - self.prefs.video_download_folder = path - except: - cmd_line(_("Failed to create default video download folder %(folder)s") % {'folder':path}) - - 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. + files_by_scan_pid = self.thumbnails.get_files_checked_for_download(scan_pid) + folders_valid, invalid_dirs = self.check_download_folder_validity(files_by_scan_pid) - returns True if program preferences window should be opened """ + if not folders_valid: + if len(invalid_dirs) > 1: + msg = _("These download folders are invalid:\n%(folder1)s\n%(folder2)s") % { + 'folder1': invalid_dirs[0], 'folder2': invalid_dirs[1]} + else: + msg = _("This download folder is invalid:\n%s") % invalid_dirs[0] + self.log_error(config.CRITICAL_ERROR, _("Download cannot proceed"), + msg) + else: + # set time download is starting if it is not already set + # it is unset when all downloads are completed + if self.download_start_time is None: + self.download_start_time = datetime.datetime.now() + + # Set status to download pending + self.thumbnails.mark_download_pending(files_by_scan_pid) + + # disable refresh and preferences change while download is occurring + self.enable_prefs_and_refresh(enabled=False) + + for scan_pid in files_by_scan_pid: + files = files_by_scan_pid[scan_pid] + # if generating thumbnails for this scan_pid, stop it + if self.thumbnails.terminate_thumbnail_generation(scan_pid): + self.thumbnails.mark_thumbnails_needed(files) + + self.download_files(files, scan_pid) + + self.set_download_action_label(is_download = False) - displayPrefs = upgraded = False + def pause_download(self): - previousVersion = self.prefs.program_version - if len(previousVersion) > 0: - # the program has been run previously for this user + self.copy_files_manager.pause() - pv = common.pythonifyVersion(previousVersion) - rv = common.pythonifyVersion(runningVersion) + # set action to display Download + if not self.download_action_is_download: + self.set_download_action_label(is_download = True) - title = PROGRAM_NAME - imageRename = subfolder = None + self.time_check.pause() - if pv != rv: - if pv > rv: - prefsOk = rn.checkPreferencesForValidity(self.prefs.image_rename, self.prefs.subfolder, self.prefs.video_rename, self.prefs.video_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 rn.checkPreferencesForValidity(self.prefs.image_rename, self.prefs.subfolder, self.prefs.video_rename, self.prefs.video_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 + def resume_download(self): + for scan_pid in self.download_active_by_scan_pid: + self.time_remaining.set_time_mark(scan_pid) + + self.time_check.set_download_mark() + + self.copy_files_manager.start() - do_not_size_icon = False - self.notification_icon_size = 48 - try: - info = pynotify.get_server_info() - except: - cmd_line(_("Warning: desktop environment notification server is incorrectly configured.")) + def download_files(self, files, scan_pid): + """ + Initiate downloading and renaming of files + """ + + # Check which file types will be downloaded for this particular process + if self.files_of_type_present(files, rpdfile.FILE_TYPE_PHOTO): + photo_download_folder = self.prefs.download_folder else: - try: - if info["name"] == 'notify-osd': - do_not_size_icon = True - except: - pass + photo_download_folder = None + + if self.files_of_type_present(files, rpdfile.FILE_TYPE_VIDEO): + video_download_folder = self.prefs.video_download_folder + else: + video_download_folder = None - if do_not_size_icon: - self.application_icon = gtk.gdk.pixbuf_new_from_file( - paths.share_dir('glade3/rapid-photo-downloader.svg')) + download_size = self.size_files_to_be_downloaded(files) + self.download_tracker.init_stats(scan_pid=scan_pid, + bytes=download_size, + no_files=len(files)) + + if self.prefs.backup_images: + download_size = download_size * (len(self.backup_devices) + 1) + + self.time_remaining.set(scan_pid, download_size) + self.time_check.set_download_mark() + + self.download_active_by_scan_pid.append(scan_pid) + + + if len(self.download_active_by_scan_pid) > 1: + self.display_summary_notification = True + + if self.auto_start_is_on and self.prefs.generate_thumbnails: + for rpd_file in files: + rpd_file.generate_thumbnail = True + + # Initiate copy files process + self.copy_files_manager.add_task((photo_download_folder, + video_download_folder, scan_pid, + files)) + + def copy_files_results(self, source, condition): + """ + Handle results from copy files process + """ + #FIXME: must handle early termination / pause of copy files process + connection = self.copy_files_manager.get_pipe(source) + conn_type, msg_data = connection.recv() + if conn_type == rpdmp.CONN_PARTIAL: + msg_type, data = msg_data + + if msg_type == rpdmp.MSG_TEMP_DIRS: + scan_pid, photo_temp_dir, video_temp_dir = data + self.temp_dirs_by_scan_pid[scan_pid] = (photo_temp_dir, video_temp_dir) + elif msg_type == rpdmp.MSG_BYTES: + scan_pid, total_downloaded, chunk_downloaded = data + self.download_tracker.set_total_bytes_copied(scan_pid, + total_downloaded) + self.time_check.increment(bytes_downloaded=chunk_downloaded) + percent_complete = self.download_tracker.get_percent_complete(scan_pid) + self.device_collection.update_progress(scan_pid, percent_complete, + None, None) + self.time_remaining.update(scan_pid, bytes_downloaded=chunk_downloaded) + elif msg_type == rpdmp.MSG_FILE: + download_succeeded, rpd_file, download_count, temp_full_file_name, thumbnail_icon, thumbnail = data + + if thumbnail is not None or thumbnail_icon is not None: + self.thumbnails.update_thumbnail((rpd_file.unique_id, + thumbnail_icon, + thumbnail)) + + self.download_tracker.set_download_count_for_file( + rpd_file.unique_id, download_count) + self.download_tracker.set_download_count( + rpd_file.scan_pid, download_count) + rpd_file.download_start_time = self.download_start_time + + if download_succeeded: + # Insert preference values needed for name generation + rpd_file = prefsrapid.insert_pref_lists(self.prefs, rpd_file) + rpd_file.strip_characters = self.prefs.strip_characters + rpd_file.download_folder = self.prefs.get_download_folder_for_file_type(rpd_file.file_type) + rpd_file.download_conflict_resolution = self.prefs.download_conflict_resolution + rpd_file.synchronize_raw_jpg = self.prefs.must_synchronize_raw_jpg() + rpd_file.job_code = self.job_code + + self.subfolder_file_manager.rename_file_and_move_to_subfolder( + download_succeeded, + download_count, + rpd_file + ) + + return True else: - self.application_icon = gtk.gdk.pixbuf_new_from_file_at_size( - paths.share_dir('glade3/rapid-photo-downloader.svg'), - self.notification_icon_size, self.notification_icon_size) + # Process is complete, i.e. conn_type == rpdmp.CONN_COMPLETE + connection.close() + return False + - - def usingVolumeMonitor(self): + def download_is_occurring(self): + """Returns True if a file is currently being downloaded, renamed or + backed up """ - 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 - )) - + return not len(self.download_active_by_scan_pid) == 0 - def startVolumeMonitor(self): - if not self.volumeMonitor: - self.volumeMonitor = VMonitor(self) + # # # + # Create folder and file names for downloaded files + # # # - def displayBackupVolumes(self): + def subfolder_file_results(self, move_succeeded, rpd_file): """ - Create a message to be displayed to the user showing which backup volumes will be used + Handle results of subfolder creation and file renaming """ - 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 + scan_pid = rpd_file.scan_pid + unique_id = rpd_file.unique_id - 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, gMount): - - """ gvfs GProxyShadowMount is used for the camera itself, not the data in the memory card """ - if using_gio: - if hasattr(gMount, 'is_shadowed'): - return gMount.is_shadowed() + if rpd_file.status == config.STATUS_DOWNLOADED_WITH_WARNING: + self.log_error(config.WARNING, rpd_file.error_title, + rpd_file.error_msg, rpd_file.error_extra_detail) + self.error_title = '' + self.error_msg = '' + self.error_extra_detail = '' + + + if self.prefs.backup_images and len(self.backup_devices): + if self.prefs.backup_device_autodetection: + if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: + path_suffix = self.prefs.backup_identifier + else: + path_suffix = self.prefs.video_backup_identifier else: - return str(type(gMount)).find('GProxyShadowMount') >= 0 + path_suffix = None + + self.backup_manager.backup_file(move_succeeded, rpd_file, + path_suffix, + self.prefs.backup_duplicate_overwrite) + else: + self.file_download_finished(move_succeeded, rpd_file) + + + def backup_results(self, source, condition): + connection = self.backup_manager.get_pipe(source) + conn_type, msg_data = connection.recv() + if conn_type == rpdmp.CONN_PARTIAL: + msg_type, data = msg_data + + if msg_type == rpdmp.MSG_BYTES: + scan_pid, backup_pid, total_downloaded, chunk_downloaded = data + self.download_tracker.increment_bytes_backed_up(scan_pid, + chunk_downloaded) + self.time_check.increment(bytes_downloaded=chunk_downloaded) + percent_complete = self.download_tracker.get_percent_complete(scan_pid) + self.device_collection.update_progress(scan_pid, percent_complete, + None, None) + self.time_remaining.update(scan_pid, bytes_downloaded=chunk_downloaded) + + elif msg_type == rpdmp.MSG_FILE: + backup_succeeded, rpd_file = data + self.download_tracker.file_backed_up(rpd_file.unique_id) + if self.download_tracker.all_files_backed_up(rpd_file.unique_id): + self.file_download_finished(backup_succeeded, rpd_file) + return True else: return False + + + def file_download_finished(self, succeeded, rpd_file): + scan_pid = rpd_file.scan_pid + unique_id = rpd_file.unique_id + # Update error log window if neccessary + if not succeeded: + self.log_error(config.SERIOUS_ERROR, rpd_file.error_title, + rpd_file.error_msg, rpd_file.error_extra_detail) + + self.thumbnails.update_status_post_download(rpd_file) + self.download_tracker.file_downloaded_increment(scan_pid, + rpd_file.file_type, + rpd_file.status) + + completed, files_remaining = self._update_file_download_device_progress(scan_pid, unique_id) + + if self.download_is_occurring(): + self.update_time_remaining() + + if completed: + # Last file for this scan pid has been downloaded, so clean temp directory + logger.debug("Purging temp directories") + self._clean_temp_dirs_for_scan_pid(scan_pid) + self.download_active_by_scan_pid.remove(scan_pid) + self.time_remaining.remove(scan_pid) + self.notify_downloaded_from_device(scan_pid) + if files_remaining == 0 and self.prefs.auto_unmount: + self.device_collection.unmount(scan_pid) + + + if not self.download_is_occurring(): + logger.debug("Download completed") + self.enable_prefs_and_refresh(enabled=True) + self.notify_download_complete() + self.download_progressbar.set_fraction(0.0) + + self.prefs.stored_sequence_no = self.stored_sequence_value.value + self.downloads_today_tracker.set_raw_downloads_today_from_int(self.downloads_today_value.value) + self.downloads_today_tracker.set_raw_downloads_today_date(self.downloads_today_date_value.value) + self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker) + + if ((self.prefs.auto_exit and self.download_tracker.no_errors_or_warnings()) + or self.prefs.auto_exit_force): + if not self.thumbnails.files_remain_to_download(): + self._terminate_processes() + gtk.main_quit() + + self.download_tracker.purge_all() + self.speed_label.set_label(" ") + + self.display_free_space() + + self.set_download_action_label(is_download=True) + self.set_download_action_sensitivity() + + self.job_code = '' + self.download_start_time = None + + + def update_time_remaining(self): + update, download_speed = self.time_check.check_for_update() + if update: + self.speed_label.set_text(download_speed) - def isCamera(self, volume): - if using_gio: - try: - return volume.get_root().query_filesystem_info(gio.FILE_ATTRIBUTE_GVFS_BACKEND).get_attribute_as_string(gio.FILE_ATTRIBUTE_GVFS_BACKEND) == 'gphoto2' - except: - return False + time_remaining = self.time_remaining.time_remaining() + if time_remaining: + secs = int(time_remaining) + + if secs == 0: + message = "" + elif secs == 1: + message = _("About 1 second remaining") + elif secs < 60: + message = _("About %i seconds remaining") % secs + elif secs == 60: + message = _("About 1 minute remaining") + else: + # Translators: in the text '%(minutes)i:%(seconds)02i', only the : should be translated, if needed. + # '%(minutes)i' and '%(seconds)02i' should not be modified or left out. They are used to format and display the amount + # of time the download has remainging, e.g. 'About 5:36 minutes remaining' + message = _("About %(minutes)i:%(seconds)02i minutes remaining") % {'minutes': secs / 60, 'seconds': secs % 60} + + self.rapid_statusbar.pop(self.statusbar_context_id) + self.rapid_statusbar.push(self.statusbar_context_id, message) + + def file_types_by_number(self, no_photos, no_videos): + """ + returns a string to be displayed to the user that can be used + to show if a value refers to photos or videos or both, or just one + of each + """ + if (no_videos > 0) and (no_photos > 0): + v = _('photos and videos') + elif (no_videos == 0) and (no_photos == 0): + v = _('photos or videos') + elif no_videos > 0: + if no_videos > 1: + v = _('videos') + else: + v = _('video') else: - return False + if no_photos > 1: + v = _('photos') + else: + v = _('photo') + return v - def workerHasThisPath(self, path): - havePath= False - for w in workers.getNonFinishedWorkers(): - if w.cardMedia.path == path: - havePath = True - break - return havePath + def notify_downloaded_from_device(self, scan_pid): + device = self.device_collection.get_device(scan_pid) + + if device.mount is None: + notification_name = PROGRAM_NAME + icon = self.application_icon + else: + notification_name = device.get_name() + icon = device.get_icon(self.notification_icon_size) + + no_photos_downloaded = self.download_tracker.get_no_files_downloaded( + scan_pid, rpdfile.FILE_TYPE_PHOTO) + no_videos_downloaded = self.download_tracker.get_no_files_downloaded( + scan_pid, rpdfile.FILE_TYPE_VIDEO) + no_photos_failed = self.download_tracker.get_no_files_failed( + scan_pid, rpdfile.FILE_TYPE_PHOTO) + no_videos_failed = self.download_tracker.get_no_files_failed( + scan_pid, rpdfile.FILE_TYPE_VIDEO) + no_files_downloaded = no_photos_downloaded + no_videos_downloaded + no_files_failed = no_photos_failed + no_videos_failed + no_warnings = self.download_tracker.get_no_warnings(scan_pid) + + file_types = self.file_types_by_number(no_photos_downloaded, no_videos_downloaded) + file_types_failed = self.file_types_by_number(no_photos_failed, no_videos_failed) + message = _("%(noFiles)s %(filetypes)s downloaded") % \ + {'noFiles':no_files_downloaded, 'filetypes': file_types} + + if no_files_failed: + message += "\n" + _("%(noFiles)s %(filetypes)s failed to download") % {'noFiles':no_files_failed, 'filetypes':file_types_failed} + + if no_warnings: + message = "%s\n%s " % (message, no_warnings) + _("warnings") + + n = pynotify.Notification(notification_name, message) + n.set_icon_from_pixbuf(icon) + + n.show() + + def notify_download_complete(self): + if self.display_summary_notification: + message = _("All downloads complete") + + # photo downloads + photo_downloads = self.download_tracker.total_photos_downloaded + if photo_downloads: + filetype = self.file_types_by_number(photo_downloads, 0) + message += "\n" + _("%(number)s %(numberdownloaded)s") % \ + {'number': photo_downloads, + 'numberdownloaded': _("%(filetype)s downloaded") % \ + {'filetype': filetype}} + + # photo failures + photo_failures = self.download_tracker.total_photo_failures + if photo_failures: + filetype = self.file_types_by_number(photo_failures, 0) + message += "\n" + _("%(number)s %(numberdownloaded)s") % \ + {'number': photo_failures, + 'numberdownloaded': _("%(filetype)s failed to download") % \ + {'filetype': filetype}} + + # video downloads + video_downloads = self.download_tracker.total_videos_downloaded + if video_downloads: + filetype = self.file_types_by_number(0, video_downloads) + message += "\n" + _("%(number)s %(numberdownloaded)s") % \ + {'number': video_downloads, + 'numberdownloaded': _("%(filetype)s downloaded") % \ + {'filetype': filetype}} + + # video failures + video_failures = self.download_tracker.total_video_failures + if video_failures: + filetype = self.file_types_by_number(0, video_failures) + message += "\n" + _("%(number)s %(numberdownloaded)s") % \ + {'number': video_failures, + 'numberdownloaded': _("%(filetype)s failed to download") % \ + {'filetype': filetype}} + + # warnings + warnings = self.download_tracker.total_warnings + if warnings: + message += "\n" + _("%(number)s %(numberdownloaded)s") % \ + {'number': warnings, + 'numberdownloaded': _("warnings")} + + n = pynotify.Notification(PROGRAM_NAME, message) + n.set_icon_from_pixbuf(self.application_icon) + n.show() + self.display_summary_notification = False # don't show it again unless needed + - def on_volume_mounted(self, monitor, mount): - """ - callback run when gnomevfs indicates a new volume - has been mounted + def _update_file_download_device_progress(self, scan_pid, unique_id): """ + Increments the progress bar for an individual device - 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: - if path not in self.backupVolumes: - self.backupVolumes[path] = volume - self.displayFreeSpace() - - elif self.prefs.device_autodetection and (media.is_DCIM_Media(path) or self.searchForPsd()): - if self.isCamera(volume.volume): - self.getShowWarningDownloadingFromCamera() - 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) - i = workers.getNextThread_id() - - workers.append(CopyPhotos(i, self, self.fileRenameLock, - self.fileSequenceLock, self.statsLock, - self.downloadedFilesLock, 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 + Returns if the download is completed for that scan_pid + It also returns the number of files remaining for the scan_pid, BUT + this value is valid ONLY if the download is completed """ - v = Volume(volume) - path = v.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: - if w.cardMedia.volume.volume == volume: - media_collection_treeview.removeCard(w.thread_id) - self.selection_vbox.selection_treeview.clear_all(w.thread_id) - workers.disableWorker(w.thread_id) - # second scenario - for w in workers.getReadyToDownloadWorkers(): - if w.cardMedia.volume: - if w.cardMedia.volume.volume == volume: - media_collection_treeview.removeCard(w.thread_id) - self.selection_vbox.selection_treeview.clear_all(w.thread_id) - workers.disableWorker(w.thread_id) - - # fourth scenario - nothing to do + files_downloaded = self.download_tracker.get_download_count_for_file(unique_id) + files_to_download = self.download_tracker.get_no_files_in_download(scan_pid) + file_types = self.download_tracker.get_file_types_present(scan_pid) + completed = files_downloaded == files_to_download + if completed and self.prefs.backup_images: + completed = self.download_tracker.all_files_backed_up(unique_id) + + if completed: + files_remaining = self.thumbnails.get_no_files_remaining(scan_pid) + else: + files_remaining = 0 - # remove backup volumes - if path in self.backupVolumes: - del self.backupVolumes[path] - self.displayFreeSpace() - - # may need to disable download button - self.setDownloadButtonSensitivity() + if completed and files_remaining: + # e.g.: 3 of 205 photos and videos (202 remaining) + progress_bar_text = _("%(number)s of %(total)s %(filetypes)s (%(remaining)s remaining)") % { + 'number': files_downloaded, + 'total': files_to_download, + 'filetypes': file_types, + 'remaining': files_remaining} + else: + # e.g.: 205 of 205 photos and videos + progress_bar_text = _("%(number)s of %(total)s %(filetypes)s") % \ + {'number': files_downloaded, + 'total': files_to_download, + 'filetypes': file_types} + percent_complete = self.download_tracker.get_percent_complete(scan_pid) + self.device_collection.update_progress(scan_pid=scan_pid, + percent_complete=percent_complete, + progress_bar_text=progress_bar_text, + bytes_downloaded=None) + + percent_complete = self.download_tracker.get_overall_percent_complete() + self.download_progressbar.set_fraction(percent_complete) + + return (completed, files_remaining) - - def clearCompletedDownloads(self): - """ - clears the display of completed downloads - """ - for w in workers.getFinishedWorkers(): - media_collection_treeview.removeCard(w.thread_id) - self.selection_vbox.selection_treeview.clear_all(w.thread_id) - - - - - def clearNotStartedDownloads(self): + def _clean_all_temp_dirs(self): """ - Clears the display of the download and instructs the thread not to run + Cleans all temp dirs if they exist """ - - for w in workers.getNotDownloadingWorkers(): - media_collection_treeview.removeCard(w.thread_id) - workers.disableWorker(w.thread_id) + for scan_pid in self.temp_dirs_by_scan_pid: + for temp_dir in self.temp_dirs_by_scan_pid[scan_pid]: + self._purge_dir(temp_dir) + + self.temp_dirs_by_scan_pid = {} + - def checkIfBackupVolume(self, path): + def _clean_temp_dirs_for_scan_pid(self, scan_pid): """ - Checks to see if backups are enabled and path represents a valid backup location - - Checks against user preferences. + Deletes temp files and folders used in download """ - identifiers = [self.prefs.backup_identifier] - if DOWNLOAD_VIDEO: - identifiers.append(self.prefs.video_backup_identifier) - if self.prefs.backup_images: - if self.prefs.backup_device_autodetection: - if media.isBackupMedia(path, identifiers): - 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): + for temp_dir in self.temp_dirs_by_scan_pid[scan_pid]: + self._purge_dir(temp_dir) + del self.temp_dirs_by_scan_pid[scan_pid] + + def _purge_dir(self, directory): """ - Sets up volumes for downloading from and backing up to + Deletes all files in the directory, and the directory itself. - 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 - being changed - - Removes any image media that are currently not downloaded, - or finished downloading + Does not recursively traverse any subfolders in the directory. """ - self.clearNotStartedDownloads() - - volumeList = [] - self.backupVolumes = {} - - if not workers.noDownloadingWorkers(): - self.downloadStats.clear() - self._resetDownloadInfo() - - if self.usingVolumeMonitor(): - # either using automatically detected backup devices - # or download devices - - for v in self.volumeMonitor.get_mounts(): - volume = Volume(v) #'volumes' are actually mounts (legacy variable name at work here) - 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[path] = volume - elif self.prefs.device_autodetection and (media.is_DCIM_Media(path) or self.searchForPsd()): - volumeList.append((path, volume)) - - - if not self.prefs.device_autodetection: - # user manually specified the path from which to download - 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 - # will backup to this path, but don't need any volume info associated with it - self.backupVolumes[self.prefs.backup_location] = None + if directory: + try: + path = gio.File(directory) + # first delete any files in the temp directory + # assume there are no directories in the temp directory + file_attributes = "standard::name,standard::type" + children = path.enumerate_children(file_attributes) + for child in children: + f = path.get_child(child.get_name()) + f.delete(cancellable=None) + path.delete(cancellable=None) + logger.debug("Deleted directory %s", directory) + except gio.Error, inst: + logger.error("Failure deleting temporary folder %s", directory) + logger.error(inst) + + + # # # + # Preferences + # # # - self.displayFreeSpace() - # 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)) + def _init_prefs(self): + self.prefs = prefsrapid.RapidPreferences() + self.prefs.notify_add(self.on_preference_changed) - self._printAutoStart(autoStart) + # flag to indicate whether the user changed some preferences that + # indicate the image and backup devices should be setup again + self.rerun_setup_available_image_and_video_media = False + self.rerun_setup_available_backup_media = False - shownWarning = False + # flag to indicate that the preferences dialog window is being + # displayed to the user + self.preferences_dialog_displayed = False - for i in range(len(volumeList)): - path, volume = volumeList[i] - if volume: - if self.isCamera(volume.volume) and not shownWarning: - self.getShowWarningDownloadingFromCamera() - shownWarning = True - 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 refreshBackupMedia(self): - """ - Setup the backup media + # flag to indicate that the user has modified the download today + # related values in the preferences dialog window + self.refresh_downloads_today = False - Assumptions: this is being called after the user has changed their preferences AND download media has already been setup - """ - self.backupVolumes = {} - if self.prefs.backup_images: - if not self.prefs.backup_device_autodetection: - # user manually specified backup location - # will backup to this path, but don't need any volume info associated with it - self.backupVolumes[self.prefs.backup_location] = None - else: - for v in self.volumeMonitor.get_mounts(): - volume = Volume(v) - path = volume.get_path(avoid_gnomeVFS_bug = True) - if path: - if self.checkIfBackupVolume(path): - # is a backup volume - if path not in self.backupVolumes: - # ensure it is not in a list of workers which have not started downloading - # if it is, remove it - for w in workers.getNotDownloadingAndNotFinishedWorkers(): - if w.cardMedia.path == path: - media_collection_treeview.removeCard(w.thread_id) - self.selection_vbox.selection_treeview.clear_all(w.thread_id) - workers.disableWorker(w.thread_id) - - downloading_workers = [] - for w in workers.getDownloadingWorkers(): - downloading_workers.append(w) - - for w in downloading_workers: - if w.cardMedia.path == path: - # the user is trying to backup to a device that is currently being downloaded from..... we don't normally allow that, but what to do? - cmd_line(_("Warning: backup device %(device)s is currently being downloaded from") % {'device': volume.get_name(limit=0)}) - - self.backupVolumes[path] = volume - - self.displayFreeSpace() + self.downloads_today_tracker = self.prefs.get_downloads_today_tracker() - def _setupDownloadbuttons(self): - self.download_hbutton_box = gtk.HButtonBox() - self.download_hbutton_box.set_spacing(12) - self.download_hbutton_box.set_homogeneous(False) - - help_button = gtk.Button(stock=gtk.STOCK_HELP) - help_button.connect("clicked", self.on_help_button_clicked) - self.download_hbutton_box.pack_start(help_button) - self.download_hbutton_box.set_child_secondary(help_button, True) - - self.DOWNLOAD_SELECTED_LABEL = _("D_ownload Selected") - 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.download_selected_button = gtk.Button() - self.download_selected_button.set_use_underline(True) - self._set_download_button() - self.download_button.connect('clicked', self.on_download_button_clicked) - self.download_selected_button.connect('clicked', self.on_download_selected_button_clicked) - self.download_hbutton_box.set_layout(gtk.BUTTONBOX_END) - self.download_hbutton_box.pack_start(self.download_selected_button) - 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_selection(self, value): - if value: - self.selection_vbox.preview_table.show_all() + downloads_today = self.downloads_today_tracker.get_and_maybe_reset_downloads_today() + if downloads_today > 0: + logger.info("Downloads that have occurred so far today: %s", downloads_today) else: - self.selection_vbox.preview_table.hide() - self.selection_vbox.set_display_preview_folders(self.prefs.display_preview_folders) - - def set_display_preview_folders(self, value): - self.selection_vbox.set_display_preview_folders(value) - - 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 - - # this next value is used by the date time option "Download Time" - self.download_start_time = None - - 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 + logger.info("No downloads have occurred so far today") - self.timeMark = self.startTime - self.sizeMark = 0 + self.downloads_today_value = Value(c_int, + self.downloads_today_tracker.get_raw_downloads_today()) + self.downloads_today_date_value = Array(c_char, + self.downloads_today_tracker.get_raw_downloads_today_date()) + self.day_start_value = Array(c_char, + self.downloads_today_tracker.get_raw_day_start()) + self.refresh_downloads_today_value = Value(c_bool, False) + self.stored_sequence_value = Value(c_int, self.prefs.stored_sequence_no) + self.uses_stored_sequence_no_value = Value(c_bool, self.prefs.any_pref_uses_stored_sequence_no()) + self.uses_session_sequece_no_value = Value(c_bool, self.prefs.any_pref_uses_session_sequece_no()) + self.uses_sequence_letter_value = Value(c_bool, self.prefs.any_pref_uses_sequence_letter_value()) - def startOrResumeWorkers(self, threads): - - # resume any paused workers - for w in workers.getPausedDownloadingWorkers(): - w.startStop() - self.timeRemaining.setTimeMark(w) - - # set the time that the download started - this is used - # in the "Download Time" date time renaming option. - self.setDownloadStartTime() - - - #start any new workers that have downloads pending - for i in threads: - workers[i].startStop() + self.prefs.program_version = __version__ - if is_beta and verbose and False: - workers.printWorkerStatus() + def _check_for_sequence_value_use(self): + self.uses_stored_sequence_no_value.value = self.prefs.any_pref_uses_stored_sequence_no() + self.uses_session_sequece_no_value.value = self.prefs.any_pref_uses_session_sequece_no() + self.uses_sequence_letter_value.value = self.prefs.any_pref_uses_sequence_letter_value() - def setDownloadStartTime(self): - if not self.download_start_time: - self.download_start_time = datetime.datetime.now() - - def updateOverallProgress(self, thread_id, bytesDownloaded, percentComplete): + def on_preference_changed(self, key, value): """ - Updates progress bar and status bar text with time remaining - to download images + Called when user changes the program's preferences """ + logger.debug("Preference change detected: %s", key) + + if key == 'show_log_dialog': + self.menu_log_window.set_active(value) + elif key in ['device_autodetection', 'device_autodetection_psd', 'device_location']: + self.rerun_setup_available_image_and_video_media = True + if not self.preferences_dialog_displayed: + self.post_preference_change() - self.totalDownloadedSoFar += bytesDownloaded - self.totalDownloadedSoFarThisRun += bytesDownloaded - - 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")) - job_code = None - if is_beta and verbose and False: - workers.printWorkerStatus() - - else: - now = time.time() - self.timeRemaining.update(thread_id, bytesDownloaded) - - 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) + elif key in ['backup_images', 'backup_device_autodetection', 'backup_location', 'backup_identifier', 'video_backup_identifier']: + self.rerun_setup_available_backup_media = True + if not self.preferences_dialog_displayed: + self.post_preference_change() - timeRemaining = self.timeRemaining.timeRemaining() - if timeRemaining: - secs = int(timeRemaining) + # Downloads today and stored sequence numbers are kept in shared memory, + # so that the subfolderfile daemon process can access and modify them + + # Note, totally ignore any changes in downloads today, as it + # is modified in a special manner via a tracking class - if secs == 0: - message = "" - elif secs == 1: - message = _("About 1 second remaining") - elif secs < 60: - message = _("About %i seconds remaining") % secs - elif secs == 60: - message = _("About 1 minute remaining") - else: - # Translators: in the text '%(minutes)i:%(seconds)02i', only the : should be translated, if needed. - # '%(minutes)i' and '%(seconds)02i' should not be modified or left out. They are used to format and display the amount - # of time the download has remainging, e.g. 'About 5:36 minutes remaining' - message = _("About %(minutes)i:%(seconds)02i minutes remaining") % {'minutes': secs / 60, 'seconds': secs % 60} - - self.rapid_statusbar.pop(self.statusbar_context_id) - self.rapid_statusbar.push(self.statusbar_context_id, message) - - - def resetSequences(self): - if self.downloadComplete(): - sequences.reset(self.prefs.getDownloadsToday(), self.prefs.stored_sequence_no) + elif key == 'stored_sequence_no': + if type(value) <> types.IntType: + logger.critical("Stored sequence number value is malformed") + else: + self.stored_sequence_value.value = value + + elif key in ['image_rename', 'subfolder', 'video_rename', 'video_subfolder']: + # Check if stored sequence no is being used + self._check_for_sequence_value_use() + + elif key in ['download_folder', 'video_download_folder']: + self.display_free_space() - def notifyUserAllDownloadsComplete(self): - """ If all downloads are complete, if needed notify the user using libnotify - - Reset progress bar info""" - - if self.downloadComplete(): - if self.displayDownloadSummaryNotification: - message = _("All downloads complete") - if self.downloadStats.noImagesDownloaded: - filetype = file_types_by_number(self.downloadStats.noImagesDownloaded, 0) - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noImagesDownloaded, - 'numberdownloaded': _("%(filetype)s downloaded") % \ - {'filetype': filetype}} - if self.downloadStats.noImagesSkipped: - filetype = file_types_by_number(self.downloadStats.noImagesSkipped, 0) - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noImagesSkipped, - 'numberdownloaded': _("%(filetype)s failed to download") % \ - {'filetype': filetype}} - if self.downloadStats.noVideosDownloaded: - filetype = file_types_by_number(0, self.downloadStats.noVideosDownloaded) - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noVideosDownloaded, - 'numberdownloaded': _("%(filetype)s downloaded") % \ - {'filetype': filetype}} - if self.downloadStats.noVideosSkipped: - filetype = file_types_by_number(0, self.downloadStats.noVideosSkipped) - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noVideosSkipped, - 'numberdownloaded': _("%(filetype)s failed to download") % \ - {'filetype': filetype}} - if self.downloadStats.noWarnings: - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noWarnings, - 'numberdownloaded': _("warnings")} - if self.downloadStats.noErrors: - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noErrors, - 'numberdownloaded': _("errors")} - - n = pynotify.Notification(PROGRAM_NAME, message) - n.set_icon_from_pixbuf(self.application_icon) - n.show() - self.displayDownloadSummaryNotification = False # don't show it again unless needed - # download statistics are cleared in exitOnDownloadComplete() - self._resetDownloadInfo() - self.speed_label.set_text(' ') - self.displayFreeSpace() + def post_preference_change(self): + if self.rerun_setup_available_image_and_video_media: + + logger.info("Download device settings preferences were changed.") + + self.thumbnails.clear_all() + self.setup_devices(on_startup = False, on_preference_change = True, block_auto_start = True) + self._set_device_collection_size() + if self.main_notebook.get_current_page() == 1: # preview of file + self.main_notebook.set_current_page(0) - def exitOnDownloadComplete(self): - if self.downloadComplete(): - if self.prefs.auto_exit: - if not (self.downloadStats.noErrors or self.downloadStats.noWarnings): - self.quit() - # since for whatever reason am not exiting, clear the download statistics - self.downloadStats.clear() - + self.rerun_setup_available_image_and_video_media = False + + if self.rerun_setup_available_backup_media: + if self.using_volume_monitor(): + self.start_volume_monitor() + logger.info("Backup preferences were changed.") + + self.refresh_backup_media() + + self.rerun_setup_available_backup_media = False + + if self.refresh_downloads_today: + self.downloads_today_value.value = self.downloads_today_tracker.get_raw_downloads_today() + self.downloads_today_date_value.value = self.downloads_today_tracker.get_raw_downloads_today_date() + self.day_start_value.value = self.downloads_today_tracker.get_raw_day_start() + self.refresh_downloads_today_value.value = True + self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker) + + - def downloadFailed(self, thread_id): - if workers.noDownloadingWorkers() == 0: - self.download_button_is_download = True - self._set_download_button() - self.setDownloadButtonSensitivity() + # # # + # Main app window management and setup + # # # - def downloadComplete(self): - return self.totalDownloadedSoFar == self.totalDownloadSize - - def setDownloadButtonSensitivity(self): + def _init_pynotify(self): + """ + Initialize system notification messages + """ + + if not pynotify.init("TestCaps"): + logger.critical("Problem using pynotify.") + gtk.main_quit() - isSensitive = (workers.noReadyToDownloadWorkers() > 0 and - workers.noScanningWorkers() == 0 and - self.selection_vbox.selection_treeview.rows_available_for_download()) or \ - workers.noDownloadingWorkers() > 0 + do_not_size_icon = False + self.notification_icon_size = 48 + try: + info = pynotify.get_server_info() + except: + logger.warning("Desktop environment notification server is incorrectly configured.") + else: + try: + if info["name"] == 'notify-osd': + do_not_size_icon = True + except: + pass - if isSensitive: - self.download_button.props.sensitive = True - # download selected button sensitity is enabled only when the user selects something - self.selection_vbox.selection_treeview.update_download_selected_button() - self.menu_download_pause.props.sensitive = True + if do_not_size_icon: + self.application_icon = gtk.gdk.pixbuf_new_from_file( + paths.share_dir('glade3/rapid-photo-downloader.svg')) else: - self.download_button.props.sensitive = False - self.download_selected_button.props.sensitive = False - self.menu_download_pause.props.sensitive = False - - return isSensitive + self.application_icon = gtk.gdk.pixbuf_new_from_file_at_size( + paths.share_dir('glade3/rapid-photo-downloader.svg'), + self.notification_icon_size, self.notification_icon_size) + + def _init_widgets(self): + """ + Initialize widgets in the main window, and variables that point to them + """ + builder = gtk.Builder() + self.builder = builder + builder.add_from_file(paths.share_dir("glade3/rapid.ui")) + self.rapidapp = builder.get_object("rapidapp") + self.main_vpaned = builder.get_object("main_vpaned") + self.main_notebook = builder.get_object("main_notebook") + self.download_action = builder.get_object("download_action") + self.download_progressbar = builder.get_object("download_progressbar") + self.rapid_statusbar = builder.get_object("rapid_statusbar") + self.statusbar_context_id = self.rapid_statusbar.get_context_id("progress") + self.device_collection_scrolledwindow = builder.get_object("device_collection_scrolledwindow") + self.next_image_action = builder.get_object("next_image_action") + self.prev_image_action = builder.get_object("prev_image_action") + self.menu_log_window = builder.get_object("menu_log_window") + self.speed_label = builder.get_object("speed_label") + self.refresh_action = builder.get_object("refresh_action") + self.preferences_action = builder.get_object("preferences_action") - def on_rapidapp_destroy(self, widget): - """Called when the application is going to quit""" + # Only enable this action when actually displaying a preview + self.next_image_action.set_sensitive(False) + self.prev_image_action.set_sensitive(False) - # save window and component sizes - self.prefs.hpaned_pos = self.selection_vbox.file_hpaned.get_position() - self.prefs.vpaned_pos = self.main_vpaned.get_position() - - x, y = self.rapidapp.get_size() - self.prefs.main_window_size_x = x - self.prefs.main_window_size_y = y - - workers.quitAllWorkers() - - self.flushevents() + # About dialog + builder.add_from_file(paths.share_dir("glade3/about.ui")) + self.about = builder.get_object("about") - display_queue.close("w") - - - def on_rapidapp_window_state_event(self, widget, event): - """ Checkto see if the user maximized the main application window or not. """ - if event.changed_mask & gdk.WINDOW_STATE_MAXIMIZED: - self.prefs.main_window_maximized = event.new_window_state & gdk.WINDOW_STATE_MAXIMIZED - + builder.connect_signals(self) + + self.preview_image = PreviewImage(self, builder) - def on_menu_clear_activate(self, widget): - self.clearCompletedDownloads() - widget.set_sensitive(False) + thumbnails_scrolledwindow = builder.get_object('thumbnails_scrolledwindow') + self.thumbnails = ThumbnailDisplay(self) + thumbnails_scrolledwindow.add(self.thumbnails) - def on_menu_refresh_activate(self, widget): - self.selection_vbox.selection_treeview.clear_all() - self.setupAvailableImageAndBackupMedia(onStartup = False, onPreferenceChange = True, doNotAllowAutoStart = True) + #collection of devices from which to download + self.device_collection_viewport = builder.get_object("device_collection_viewport") + self.device_collection = DeviceCollection(self) + self.device_collection_viewport.add(self.device_collection) - def on_menu_report_problem_activate(self, widget): - webbrowser.open("https://bugs.launchpad.net/rapid") + #error log window + self.error_log = errorlog.ErrorLog(self) + + # monitor to handle mounts and dismounts + self.vmonitor = None + # track scan ids for mount paths - very useful when a device is unmounted + self.mounts_by_path = {} + + # Download action state + self.download_action_is_download = True + + # Track the time a download commences + self.download_start_time = None + + # Whether a system wide notifcation message should be shown + # after a download has occurred in parallel + self.display_summary_notification = False + + # Values used to display how much longer a download will take + self.time_remaining = downloadtracker.TimeRemaining() + self.time_check = downloadtracker.TimeCheck() - def 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 _set_window_size(self): + """ + Remember the window size from the last time the program was run, or + set a default size + """ - def on_menu_log_window_toggled(self, widget): - active = widget.get_active() - self.prefs.show_log_dialog = active - if active: - log_dialog.widget.show() + if self.prefs.main_window_maximized: + self.rapidapp.maximize() + self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH, + config.DEFAULT_WINDOW_HEIGHT) + elif self.prefs.main_window_size_x > 0: + self.rapidapp.set_default_size(self.prefs.main_window_size_x, self.prefs.main_window_size_y) else: - log_dialog.widget.hide() + # set a default size + self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH, + config.DEFAULT_WINDOW_HEIGHT) + - def on_menu_display_selection_toggled(self, check_button): - self.prefs.display_selection = check_button.get_active() + def _set_device_collection_size(self): + """ + Set the size of the device collection scrolled window widget + """ + if self.device_collection.map_process_to_row: + height = self.device_collection_viewport.size_request()[1] + self.device_collection_scrolledwindow.set_size_request(-1, height) + self.main_vpaned.set_position(height) + else: + # don't allow the media collection to be absolutely empty + self.device_collection_scrolledwindow.set_size_request(-1, 47) - def on_menu_preview_folders_toggled(self, check_button): - self.prefs.display_preview_folders = check_button.get_active() + + def on_rapidapp_window_state_event(self, widget, event): + """ Records the window maximization state in the preferences.""" - def on_menu_zoom_out_activate(self, widget): - self.selection_vbox.zoom_out() + if event.changed_mask & gdk.WINDOW_STATE_MAXIMIZED: + self.prefs.main_window_maximized = event.new_window_state & gdk.WINDOW_STATE_MAXIMIZED - def on_menu_zoom_in_activate(self, widget): - self.selection_vbox.zoom_in() + def _setup_buttons(self): + thumbnails_button = self.builder.get_object("thumbnails_button") + image = gtk.image_new_from_file(paths.share_dir('glade3/thumbnails_icon.png')) + thumbnails_button.set_image(image) - def on_menu_select_all_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('all') - - def on_menu_select_all_photos_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('photos') + preview_button = self.builder.get_object("preview_button") + image = gtk.image_new_from_file(paths.share_dir('glade3/photo_icon.png')) + preview_button.set_image(image) + + next_image_button = self.builder.get_object("next_image_button") + image = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_BUTTON) + next_image_button.set_image(image) + + prev_image_button = self.builder.get_object("prev_image_button") + image = gtk.image_new_from_stock(gtk.STOCK_GO_BACK, gtk.ICON_SIZE_BUTTON) + prev_image_button.set_image(image) + + def _setup_icons(self): + icons = ['rapid-photo-downloader-jobcode',] + icon_list = [(icon, paths.share_dir('glade3/%s.svg' % icon)) for icon in icons] + register_iconsets(icon_list) - def on_menu_select_all_videos_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('videos') + def _setup_error_icons(self): + """ + hide display of warning and error symbols in the taskbar until they + are needed + """ + self.error_image = self.builder.get_object("error_image") + self.warning_image = self.builder.get_object("warning_image") + self.warning_vseparator = self.builder.get_object("warning_vseparator") + self.error_image.hide() + self.warning_image.hide() + self.warning_vseparator.hide() - def on_menu_select_none_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('none') + def enable_prefs_and_refresh(self, enabled): + """ + If enable is true, then the user is able to activate the preferences + or refresh command. + The intention is to be able to disable this during a download + """ + self.refresh_action.set_sensitive(enabled) + self.preferences_action.set_sensitive(enabled) + + def statusbar_message(self, msg): + self.rapid_statusbar.push(self.statusbar_context_id, msg) - def on_menu_select_all_with_job_code_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('withjobcode') - - def on_menu_select_all_without_job_code_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('withoutjobcode') - + def statusbar_message_remove(self): + self.rapid_statusbar.pop(self.statusbar_context_id) - 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): + def display_backup_mounts(self): """ - Sets download button to appropriate state + Create a message to be displayed to the user showing which backup + mounts will be used """ + message = '' - if self.download_button_is_download: - # This text will be displayed to the user on the Download / Pause button. - self.download_selected_button.set_label(self.DOWNLOAD_SELECTED_LABEL) - self.download_selected_button.set_image(gtk.image_new_from_stock( - gtk.STOCK_CONVERT, - gtk.ICON_SIZE_BUTTON)) - self.selection_vbox.selection_treeview.update_download_selected_button() - - self.download_button.set_image(gtk.image_new_from_stock( - gtk.STOCK_CONVERT, - gtk.ICON_SIZE_BUTTON)) - - if workers.noPausedWorkers(): - self.download_button.set_label(_("_Resume")) - self.download_selected_button.hide() - else: - self.download_button.set_label(_("_Download All")) - self.download_selected_button.show_all() - + paths = self.backup_devices.keys() + i = 0 + v = len(paths) + prefix = '' + for b in paths: + if v > 1: + if i < (v -1) and i > 0: + prefix = ', ' + elif i == (v - 1) : + prefix = " " + _("and") + " " + i += 1 + message = "%s%s'%s'" % (message, prefix, self.backup_devices[b].get_name()) + + if v > 1: + message = _("Using backup devices") + " %s" % message + elif v == 1: + message = _("Using backup device") + " %s" % message 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")) - self.download_selected_button.set_sensitive(False) - self.download_selected_button.hide() + message = _("No backup devices detected") - def on_menu_download_pause_activate(self, widget): - self.on_download_button_clicked(widget) + return message - 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, threads): - self.startOrResumeWorkers(threads) - 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): + def display_free_space(self): """ - Handle download button click. - - Button is in one of three states: download all, resume, or pause. + Displays the amount of space free on the filesystem the files will be + downloaded to. - If download, a click indicates to start or resume a download run. - If pause, a click indicates to pause all running downloads. + Also displays backup volumes / path being used. """ - if self.download_button_is_download: - if need_job_code_for_renaming and self.selection_vbox.selection_treeview.job_code_missing(False) and not self.prompting_for_job_code: - self.getJobCode(autoStart=False, downloadSelected=False) - else: - threads = self.selection_vbox.selection_treeview.set_status_to_download_pending(selected_only = False) - self.startDownload(threads) - self._set_download_button() + photo_dir = self.is_valid_download_dir(path=self.prefs.download_folder, is_photo_dir=True, show_error_in_log=True) + video_dir = self.is_valid_download_dir(path=self.prefs.video_download_folder, is_photo_dir=False, show_error_in_log=True) + if photo_dir and video_dir: + same_file_system = self.same_file_system(self.prefs.download_folder, + self.prefs.video_download_folder) else: - self.pauseDownload() - - def on_download_selected_button_clicked(self, widget): - # set the status of the selected workers to be downloading pending - if need_job_code_for_renaming and self.selection_vbox.selection_treeview.job_code_missing(True) and not self.prompting_for_job_code: - self.getJobCode(autoStart=False, downloadSelected=True) - else: - threads = self.selection_vbox.selection_treeview.set_status_to_download_pending(selected_only = True) - self.startDownload(threads) + same_file_system = False - + dirs = [] + if photo_dir: + dirs.append((self.prefs.download_folder, _("photos"))) + if video_dir and not same_file_system: + dirs.append((self.prefs.video_download_folder, _("videos"))) + + msg = '' + if len(dirs) > 1: + msg = ' ' + _('Free space:') + ' ' + + for i in range(len(dirs)): + dir_info = dirs[i] + folder = gio.File(dir_info[0]) + file_info = folder.query_filesystem_info(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE) + size = file_info.get_attribute_uint64(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE) + free = format_size_for_user(bytes=size) + if len(dirs) > 1: + #(videos) or (photos) will be appended to the free space message displayed to the + #user in the status bar. + #you should only translate this if your language does not use parantheses + file_type = _("(%(file_type)s)") % {'file_type': dir_info[1]} + + #Freespace available on the filesystem for downloading to + #Displayed in status bar message on main window + msg += _("%(free)s %(file_type)s") % {'free': free, 'file_type': file_type} + if i == 0: + #Inserted in the middle of the statusbar message concerning the amount of freespace + #Used to differentiate between two different file systems + #e.g. Free space: 21.3GB (photos); 14.7GB (videos). + msg += _("; ") + else: + #Inserted at the end of the statusbar message concerning the amount of freespace + #Used to differentiate between two different file systems + #e.g. Free space: 21.3GB (photos); 14.7GB (videos). + msg += _(".") + + else: + #Freespace available on the filesystem for downloading to + #Displayed in status bar message on main window + #e.g. 14.7GB available + msg = " " + _("%(free)s free") % {'free': free} - def on_help_button_clicked(self, widget): - webbrowser.open("http://www.damonlynch.net/rapid/help.html") - def on_preference_changed(self, key, value): + if self.prefs.backup_images: + if not self.prefs.backup_device_autodetection: + # user manually specified backup location + msg2 = _('Backing up to %(path)s') % {'path':self.prefs.backup_location} + else: + msg2 = self.display_backup_mounts() + + if msg: + msg = _("%(freespace)s. %(backuppaths)s.") % {'freespace': msg, 'backuppaths': msg2} + else: + msg = msg2 + + msg = msg.rstrip() + + self.statusbar_message(msg) + + def log_error(self, severity, problem, details, extra_detail=None): """ - Called when user changes the program's preferences + Display error and warning messages to user in log window """ + self.error_log.add_message(severity, problem, details, extra_detail) - if key == 'display_selection': - self.set_display_selection(value) - elif key == 'display_preview_folders': - self.set_display_preview_folders(value) - elif key == 'show_log_dialog': - self.menu_log_window.set_active(value) - elif key in ['device_autodetection', 'device_autodetection_psd', 'device_location']: - self.rerunSetupAvailableImageAndVideoMedia = True - if not self.preferencesDialogDisplayed: - self.postPreferenceChange() - - elif key in ['backup_images', 'backup_device_autodetection', 'backup_location', 'backup_identifier', 'video_backup_identifier']: - self.rerunSetupAvailableBackupMedia = True - if not self.preferencesDialogDisplayed: - self.postPreferenceChange() - - elif key in ['subfolder', 'image_rename', 'video_subfolder', 'video_rename']: - global need_job_code_for_renaming - need_job_code_for_renaming = self.needJobCodeForRenaming() - self.selection_vbox.set_job_code_display() - self.menu_select_all_without_job_code.set_sensitive(need_job_code_for_renaming) - self.menu_select_all_with_job_code.set_sensitive(need_job_code_for_renaming) - self.refreshGeneratedSampleSubfolderAndName = True - - if not self.preferencesDialogDisplayed: - self.postPreferenceChange() - - elif key in ['download_folder', 'video_download_folder']: - self.refreshSampleDownloadFolder = True - if not self.preferencesDialogDisplayed: - self.postPreferenceChange() - - elif key == 'job_codes': - # update job code list in left pane - self.selection_vbox.update_job_code_combo() + + def on_error_eventbox_button_press_event(self, widget, event): + self.prefs.show_log_dialog = True + self.error_log.widget.show() + + def on_menu_log_window_toggled(self, widget): + active = widget.get_active() + self.prefs.show_log_dialog = active + if active: + self.error_log.widget.show() + else: + self.error_log.widget.hide() - def postPreferenceChange(self): + def notify_prefs_are_invalid(self, details): + title = _("Program preferences are invalid") + logger.critical(title) + self.log_error(severity=config.CRITICAL_ERROR, problem=title, + details=details) + + + # # # + # Utility functions + # # # + + def files_of_type_present(self, files, file_type): + """ + Returns true if there is at least one instance of the file_type + in the list of files to be copied + """ + for rpd_file in files: + if rpd_file.file_type == file_type: + return True + return False + + def size_files_to_be_downloaded(self, files): + """ + Returns the total size of the files to be downloaded in bytes """ - Handle changes in program preferences after the preferences dialog window has been closed + size = 0 + for i in range(len(files)): + size += files[i].size + + return size + + def check_download_folder_validity(self, files_by_scan_pid): """ - if self.rerunSetupAvailableImageAndVideoMedia: - if self.usingVolumeMonitor(): - self.startVolumeMonitor() - cmd_line("\n" + _("Download device settings preferences were changed.")) + Checks validity of download folders based on the file types the user + is attempting to download. + + If valid, returns a tuple of True and an empty list. + If invalid, returns a tuple of False and a list of the invalid directores. + """ + valid = True + invalid_dirs = [] + # first, check what needs to be downloaded - photos and / or videos + need_photo_folder = False + need_video_folder = False + while not need_photo_folder and not need_video_folder: + for scan_pid in files_by_scan_pid: + files = files_by_scan_pid[scan_pid] + if not need_photo_folder: + if self.files_of_type_present(files, rpdfile.FILE_TYPE_PHOTO): + need_photo_folder = True + if not need_video_folder: + if self.files_of_type_present(files, rpdfile.FILE_TYPE_VIDEO): + need_video_folder = True - self.selection_vbox.selection_treeview.clear_all() - self.setupAvailableImageAndBackupMedia(onStartup = False, onPreferenceChange = True, doNotAllowAutoStart = True) - if is_beta and verbose and False: - workers.printWorkerStatus() + # second, check validity + if need_photo_folder: + if not self.is_valid_download_dir(self.prefs.download_folder, + is_photo_dir=True): + valid = False + invalid_dirs.append(self.prefs.download_folder) - self.rerunSetupAvailableImageAndVideoMedia = False - - if self.rerunSetupAvailableBackupMedia: - if self.usingVolumeMonitor(): - self.startVolumeMonitor() - cmd_line("\n" + _("Backup preferences were changed.")) - - self.refreshBackupMedia() - self.rerunSetupAvailableBackupMedia = False - - if self.refreshGeneratedSampleSubfolderAndName: - cmd_line("\n" + _("Subfolder and filename preferences were changed.")) - for w in workers.getScanningWorkers(): - if not w.scanResultsStale: - w.scanResultsStale = True - self.noAfterScanRefreshGeneratedSampleSubfolderAndName += 1 + if need_video_folder: + if not self.is_valid_download_dir(self.prefs.video_download_folder, + is_photo_dir=False): + valid = False + invalid_dirs.append(self.prefs.video_download_folder) - self.selection_vbox.selection_treeview.refreshGeneratedSampleSubfolderAndName() - self.refreshGeneratedSampleSubfolderAndName = False - self.setDownloadButtonSensitivity() - - if self.refreshSampleDownloadFolder: - cmd_line("\n" + _("Download folder preferences were changed.")) - for w in workers.getScanningWorkers(): - if not w.scanResultsStaleDownloadFolder: - w.scanResultsStaleDownloadFolder = True - self.noAfterScanRefreshSampleDownloadFolders += 1 - - self.selection_vbox.selection_treeview.refreshSampleDownloadFolders() - self.refreshSampleDownloadFolder = False + return (valid, invalid_dirs) - def regenerateScannedDevices(self, thread_id): + def same_file_system(self, file1, file2): + """Returns True if the files / diretories are on the same file system """ - Regenerate the filenames / subfolders / download folders for this thread + f1 = gio.File(file1) + f2 = gio.File(file2) + f1_info = f1.query_info(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) + f1_id = f1_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) + f2_info = f2.query_info(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) + f2_id = f2_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) + return f1_id == f2_id - The user must have adjusted their preferences as the device was being scanned + + def same_file(self, file1, file2): + """Returns True if the files / directories are the same """ + f1 = gio.File(file1) + f2 = gio.File(file2) - if self.noAfterScanRefreshSampleDownloadFolders: - # no point updating it if we're going to update it in the - # refresh of sample names and subfolders anway! - if not self.noAfterScanRefreshGeneratedSampleSubfolderAndName: - self.selection_vbox.selection_treeview.refreshSampleDownloadFolders(thread_id) - self.noAfterScanRefreshSampleDownloadFolders -= 1 - - if self.noAfterScanRefreshGeneratedSampleSubfolderAndName: - self.selection_vbox.selection_treeview.refreshGeneratedSampleSubfolderAndName(thread_id) - self.noAfterScanRefreshGeneratedSampleSubfolderAndName -= 1 - - + file_attributes = "id::file" + f1_info = f1.query_filesystem_info(file_attributes) + f1_id = f1_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILE) + f2_info = f2.query_filesystem_info(file_attributes) + f2_id = f2_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILE) + return f1_id == f2_id - - def 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) + def is_valid_download_dir(self, path, is_photo_dir, show_error_in_log=False): + """ + Checks the following conditions: + Does the directory exist? + Is it writable? + + if show_error_in_log is True, then display warning in log window, using + is_photo_dir, which if true means the download directory is for photos, + if false, for Videos + """ + valid = False + if is_photo_dir: + download_folder_type = _("Photo") else: - self.vmonitor = gnomevfs.VolumeMonitor() - self.vmonitor.connect("volume-mounted", self.app.on_volume_mounted) - self.vmonitor.connect("volume-unmounted", self.app.on_volume_unmounted) + download_folder_type = _("Video") + try: + d = gio.File(path) + if not d.query_exists(cancellable=None): + logger.error("%s download folder does not exist: %s", + download_folder_type, path) + if show_error_in_log: + severity = config.WARNING + problem = _("%(file_type)s download folder does not exist") % { + 'file_type': download_folder_type} + details = _("Folder: %s") % path + self.log_error(severity, problem, details) + else: + file_attributes = "standard::type,access::can-read,access::can-write" + file_info = d.query_filesystem_info(file_attributes) + file_type = file_info.get_file_type() + + if file_type != gio.FILE_TYPE_DIRECTORY and file_type != gio.FILE_TYPE_UNKNOWN: + logger.error("%s download folder is invalid: %s", + download_folder_type, path) + if show_error_in_log: + severity = config.WARNING + problem = _("%(file_type)s download folder is invalid") % { + 'file_type': download_folder_type} + details = _("Folder: %s") % path + self.log_error(severity, problem, details) + else: + # is the directory writable? + try: + temp_dir = tempfile.mkdtemp(prefix="rpd-tmp", dir=path) + valid = True + except: + logger.error("%s is not writable", path) + if show_error_in_log: + severity = config.WARNING + problem = _("%(file_type)s download folder is not writable") % { + 'file_type': download_folder_type} + details = _("Folder: %s") % path + self.log_error(severity, problem, details) + else: + f = gio.File(temp_dir) + f.delete(cancellable=None) - def get_mounts(self): - if using_gio: - return self.vmonitor.get_mounts() - else: - return self.vmonitor.get_mounted_volumes() + except gio.Error, inst: + logger.error("Error checking download directory %s", path) + logger.error(inst) -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 + return valid + + + + # # # + # Process results and management + # # # - 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 _start_process_managers(self): + """ + Set up process managers. - def get_icon_pixbuf(self, size): - """ returns icon for the volume, or None if not available""" + A task such as scanning a device or copying files is handled in its + own process. + """ - return common.get_icon_pixbuf(using_gio, self.volume.get_icon(), size) - - def unmount(self, callback): - self.volume.unmount(callback) - -class DownloadStats: - def __init__(self): - self.clear() + self.batch_size = 10 + self.batch_size_MB = 2 - def adjust(self, size, noImagesDownloaded, noVideosDownloaded, noImagesSkipped, noVideosSkipped, noWarnings, noErrors): - self.downloadSize += size - self.noImagesDownloaded += noImagesDownloaded - self.noVideosDownloaded += noVideosDownloaded - self.noImagesSkipped += noImagesSkipped - self.noVideosSkipped += noVideosSkipped - self.noWarnings += noWarnings - self.noErrors += noErrors - - def clear(self): - self.noImagesDownloaded = self.noVideosDownloaded = self.noImagesSkipped = self.noVideosSkipped = 0 - self.downloadSize = 0 - self.noWarnings = self.noErrors = 0 - -class DownloadedFiles: - def __init__(self): - self.images = {} - - def add_download(self, name, extension, date_time, sub_seconds, sequence_number_used): - if name not in self.images: - self.images[name] = ([extension], date_time, sub_seconds, sequence_number_used) - else: - if extension not in self.images[name][0]: - self.images[name][0].append(extension) - + sequence_values = (self.downloads_today_value, + self.downloads_today_date_value, + self.day_start_value, + self.refresh_downloads_today_value, + self.stored_sequence_value, + self.uses_stored_sequence_no_value, + self.uses_session_sequece_no_value, + self.uses_sequence_letter_value) + + self.subfolder_file_manager = SubfolderFileManager( + self.subfolder_file_results, + sequence_values) + - def matching_pair(self, name, extension, date_time, sub_seconds): - """Checks to see if the image matches an image that has already been downloaded. - Image name (minus extension), exif date time, and exif subseconds are checked. + self.generate_folder = False + self.scan_manager = ScanManager(self.scan_results, self.batch_size, + self.generate_folder, self.device_collection.add_device) + self.copy_files_manager = CopyFilesManager(self.copy_files_results, + self.batch_size_MB) + self.backup_manager = BackupFilesManager(self.backup_results, + self.batch_size_MB) - Returns -1 and a sequence number if the name, extension, and exif values match (i.e. it has already been downloaded) - Returns 0 and a sequence number if name and exif values match, but the extension is different (i.e. a matching RAW + JPG image) - Returns -99 and a sequence number of None if images detected with the same filenames, but taken at different times - Returns 1 and a sequence number of None if no match""" - if name in self.images: - if self.images[name][1] == date_time and self.images[name][2] == sub_seconds: - if extension in self.images[name][0]: - return (-1, self.images[name][3]) - else: - return (0, self.images[name][3]) - else: - return (-99, None) - return (1, None) + def scan_results(self, source, condition): + """ + Receive results from scan processes + """ + connection = self.scan_manager.get_pipe(source) - def extExifDateTime(self, name): - """Returns first extension, exif date time and subseconds data for the already downloaded image""" - return (self.images[name][0][0], self.images[name][1], self.images[name][2]) + conn_type, data = connection.recv() -class TimeForDownload: - # used to store variables, see below - pass + if conn_type == rpdmp.CONN_COMPLETE: + connection.close() + self.scan_manager.no_tasks -= 1 + size, file_type_counter, scan_pid = data + size = format_size_for_user(bytes=size) + results_summary, file_types_present = file_type_counter.summarize_file_count() + self.download_tracker.set_file_types_present(scan_pid, file_types_present) + logger.info('Found %s' % results_summary) + logger.info('Files total %s' % size) + self.device_collection.update_device(scan_pid, size) + self.device_collection.update_progress(scan_pid, 0.0, results_summary, 0) + self.set_download_action_sensitivity() + + if (not self.auto_start_is_on and + self.prefs.generate_thumbnails): + self.download_progressbar.set_text(_("Thumbnails")) + self.thumbnails.generate_thumbnails(scan_pid) + elif self.auto_start_is_on: + if self.need_job_code_for_naming and not self.job_code: + self.get_job_code() + else: + self.start_download(scan_pid=scan_pid) -class TimeRemaining: - gap = 2 - def __init__(self): - self.clear() - - def set(self, w, size): - 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 / float(amtTime) - amtToDownload = float(self.times[w].size) - self.times[w].downloaded - if timefraction: - self.times[w].timeRemaining = amtToDownload / timefraction - - def _timeEstimates(self): - for t in self.times: - yield self.times[t].timeRemaining + self.set_thumbnail_sort() - def timeRemaining(self): - return max(self._timeEstimates()) - - def setTimeMark(self, w): - if w in self.times: - self.times[w].timeMark = time.time() + # signal that no more data is coming, finishing io watch for this pipe + return False + else: + if len(data) > self.batch_size: + logger.critical("incoming pipe length is unexpectedly long: %s" % len(data)) + else: + for rpd_file in data: + self.thumbnails.add_file(rpd_file=rpd_file, + generate_thumbnail = not self.auto_start_is_on) - def clear(self): - self.times = {} + # must return True for this method to be called again + return True - def remove(self, w): - if w in self.times: - del self.times[w] -def programStatus(): - print _("Goodbye") + @dbus.service.method (config.DBUS_NAME, + in_signature='', out_signature='b') + def is_running (self): + return self.running + + @dbus.service.method (config.DBUS_NAME, + in_signature='', out_signature='') + def start (self): + if self.is_running(): + self.rapidapp.present() + else: + self.running = True + gtk.main() -def start (): - global is_beta - is_beta = config.version.find('~b') > 0 +def start(): + + is_beta = config.version.find('~') > 0 - parser = OptionParser(version= "%%prog %s" % config.version) + parser = OptionParser(version= "%%prog %s" % utilities.human_readable_version(config.version)) parser.set_defaults(verbose=is_beta, extensions=False) # Translators: this text is displayed to the user when they request information on the command line options. # The text %default should not be modified or left out. @@ -6468,19 +3277,18 @@ def start (): parser.add_option("-e", "--extensions", action="store_true", dest="extensions", help=_("list photo and video file extensions the program recognizes and exit")) parser.add_option("--reset-settings", action="store_true", dest="reset", help=_("reset all program settings and preferences and exit")) (options, args) = parser.parse_args() - global verbose - verbose = options.verbose - global debug_info - debug_info = options.debug - if debug_info: - verbose = True + if options.debug: + logging_level = logging.DEBUG + elif options.verbose: + logging_level = logging.INFO + else: + logging_level = logging.ERROR - if verbose: - atexit.register(programStatus) - + logger.setLevel(logging_level) + if options.extensions: - extensions = ((metadata.RAW_FILE_EXTENSIONS + metadata.NON_RAW_IMAGE_FILE_EXTENSIONS, _("Photos:")), (videometadata.VIDEO_FILE_EXTENSIONS, _("Videos:"))) + extensions = ((rpdfile.RAW_FILE_EXTENSIONS + rpdfile.NON_RAW_IMAGE_FILE_EXTENSIONS, _("Photos:")), (rpdfile.VIDEO_FILE_EXTENSIONS, _("Videos:"))) for exts, file_type in extensions: v = '' for e in exts[:-1]: @@ -6496,43 +3304,25 @@ def start (): print _("All settings and preferences have been reset") sys.exit(0) - cmd_line(_("Rapid Photo Downloader") + " %s" % config.version) - cmd_line(_("Using") + " pyexiv2 " + metadata.version_info()) - cmd_line(_("Using") + " exiv2 " + metadata.exiv2_version_info()) + logger.info("Rapid Photo Downloader %s", utilities.human_readable_version(config.version)) + logger.info("Using pyexiv2 %s", metadataphoto.pyexiv2_version_info()) + logger.info("Using exiv2 %s", metadataphoto.exiv2_version_info()) if DOWNLOAD_VIDEO: - cmd_line(_("Using") + " hachoir " + videometadata.version_info()) + logger.info("Using hachoir %s", metadatavideo.version_info()) else: - cmd_line(_("\n" + "Video downloading functionality disabled.\nTo download videos, please install the hachoir metadata and kaa metadata packages for python.") + "\n") - - if using_gio: - cmd_line(_("Using") + " GIO") - gobject.threads_init() - else: - # Which volume management code is being used (GIO or GnomeVFS) - cmd_line(_("Using") + " GnomeVFS") - gdk.threads_init() - - + logger.info(_("Video downloading functionality disabled.\nTo download videos, please install the hachoir metadata and kaa metadata packages for python.")) - 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) + 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 + print "Rapid Photo Downloader is already running" object = bus.get_object (config.DBUS_NAME, "/") app = dbus.Interface (object, config.DBUS_NAME) - app.start() - - gdk.threads_leave() + app.start() if __name__ == "__main__": start() |