summaryrefslogtreecommitdiff
path: root/rapid/rapid.py
diff options
context:
space:
mode:
Diffstat (limited to 'rapid/rapid.py')
-rwxr-xr-xrapid/rapid.py8102
1 files changed, 5976 insertions, 2126 deletions
diff --git a/rapid/rapid.py b/rapid/rapid.py
index 3603025..f41c79f 100755
--- a/rapid/rapid.py
+++ b/rapid/rapid.py
@@ -1,7 +1,7 @@
#!/usr/bin/python
# -*- coding: latin1 -*-
-### Copyright (C) 2011 Damon Lynch <damonlynch@gmail.com>
+### Copyright (C) 2007, 2008, 2009, 2010 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,8 +17,19 @@
### 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
@@ -26,89 +37,1549 @@ import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
DBusGMainLoop(set_as_default=True)
-from optparse import OptionParser
+from threading import Thread, Lock
+from thread import error as thread_error
+from thread import get_ident
-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
-import webbrowser
-
-import sys, time, types, os, datetime
+from optparse import OptionParser
-import gobject, pango, cairo, array, pangocairo, gio
+import pynotify
-from multiprocessing import Process, Pipe, Queue, Event, Value, Array, current_process, log_to_stderr
-from ctypes import c_int, c_bool, c_char
+import idletube as tube
-import logging
-logger = log_to_stderr()
+import config
-# Rapid Photo Downloader modules
+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
-import rpdfile
+from media import getDefaultPhotoLocation, getDefaultVideoLocation, \
+ getDefaultBackupPhotoIdentifier, \
+ getDefaultBackupVideoIdentifier
-import problemnotification as pn
-import thumbnail as tn
-import rpdmultiprocessing as rpdmp
+import ValidatedEntry
+
+from media import CardMedia
-import preferencesdialog
-import prefsrapid
+import media
+
+import metadata
+import videometadata
+from videometadata import DOWNLOAD_VIDEO
+
+import renamesubfolderprefs as rn
+import problemnotification as pn
import tableplusminus as tpm
-import generatename as gn
-import downloadtracker
+__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')
-from metadatavideo import DOWNLOAD_VIDEO
-import metadataphoto
-import metadatavideo
+TINY_SCREEN = gtk.gdk.screen_height() <= config.TINY_SCREEN_HEIGHT
+#~ TINY_SCREEN = True
-import scan as scan_process
-import copyfiles
-import subfolderfile
+def today():
+ return datetime.date.today().strftime('%Y-%m-%d')
-import errorlog
-import device as dv
-import utilities
-import config
-__version__ = config.version
+def cmd_line(msg):
+ if verbose:
+ print msg
-import paths
+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
-import gettext
-gettext.bindtextdomain(config.APP_NAME)
-gettext.textdomain(config.APP_NAME)
-from gettext import gettext as _
+class Queue(tube.Tube):
+ def __init__(self, maxSize = config.MAX_NO_READERS):
+ tube.Tube.__init__(self, maxSize)
+ def setMaxSize(self, maxSize):
+ self.maxsize = maxSize
-from utilities import format_size_for_user
-from utilities import register_iconsets
+# 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 :(
-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
+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
+
+ 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")
-DOWNLOADED = [STATUS_DOWNLOADED, STATUS_DOWNLOADED_WITH_WARNING, STATUS_BACKUP_PROBLEM]
+ self.table_type = self.errorTitle[len("Error in "):]
+ self.i = 0
-#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
+ 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
+
+ def updateExample(self):
+ self.parentApp.updateVideoRenameExample()
+
+class SubfolderTable(ImageRenameTable):
+ def __init__(self, parentApp, adjustScollWindow):
+ self.errorTitle = _("Error in Photo Download Subfolders preferences")
+ ImageRenameTable.__init__(self, parentApp, adjustScollWindow)
+
+ def getParentAppPrefs(self):
+ self.prefList = self.parentApp.prefs.subfolder
+
+ def getPrefsFactory(self):
+ self.prefsFactory = rn.SubfolderPreferences(self.prefList, self)
+
+ def updateParentAppPrefs(self):
+ self.parentApp.prefs.subfolder = self.prefList
+
+ def updateExample(self):
+ self.parentApp.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
+
+ 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")
+
+ # 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):
+
+ 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):
+
+ 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)
+
+
+ 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>")
+
+ 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()
+
+ 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")
+
+ 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)
+
+
+ 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)
+
+
+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"),
@@ -117,19 +1588,1498 @@ 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 DeviceCollection(gtk.TreeView):
+class NeedAJobCode():
"""
- TreeView display of devices and how many files have been copied, shown
- immediately under the menu in the main application window.
+ Convenience class to check whether a job code is missing for a given
+ file type (photo or video)
"""
- def __init__(self, parent_app):
+ 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)
+
- self.parent_app = parent_app
- # device icon & name, size of images on the device (human readable),
- # copy progress (%), copy text
- self.liststore = gtk.ListStore(gtk.gdk.Pixbuf, str, str, float, str)
- self.map_process_to_row = {}
+ def initializeDisplay(self, thread_id, cardMedia = None):
+
+ 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 = {}
gtk.TreeView.__init__(self, self.liststore)
@@ -138,357 +3088,587 @@ class DeviceCollection(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"))
- pixbuf_renderer = gtk.CellRendererPixbuf()
- text_renderer = gtk.CellRendererText()
- text_renderer.props.ellipsize = pango.ELLIPSIZE_MIDDLE
- text_renderer.set_fixed_size(160, -1)
- column0.pack_start(pixbuf_renderer, expand=False)
- column0.pack_start(text_renderer, expand=True)
- column0.add_attribute(pixbuf_renderer, 'pixbuf', 0)
- column0.add_attribute(text_renderer, 'text', 1)
+ # Device refers to a thing like a camera, memory card in its reader, external hard drive, Portable Storage Device, etc.
+ column0 = gtk.TreeViewColumn(_("Device"), gtk.CellRendererText(),
+ text=0)
self.append_column(column0)
-
- # Size refers to the total size of images on the device, typically in
- # MB or GB
- column1 = gtk.TreeViewColumn(_("Size"), gtk.CellRendererText(), text=2)
+ # Size refers to the total size of images on the device, typically in MB or GB
+ column1 = gtk.TreeViewColumn(_("Size"), gtk.CellRendererText(), text=1)
self.append_column(column1)
column2 = gtk.TreeViewColumn(_("Download Progress"),
- gtk.CellRendererProgress(),
- value=3,
- text=4)
+ gtk.CellRendererProgress(), value=2, text=3)
self.append_column(column2)
self.show_all()
- def add_device(self, process_id, device, progress_bar_text = ''):
+ def addCard(self, thread_id, cardName, sizeFiles, progress = 0.0, progressBarText = ''):
# add the row, and get a temporary pointer to the row
- size_files = ''
- progress = 0.0
- iter = self.liststore.append((device.get_icon(),
- device.get_name(),
- size_files,
- progress,
- progress_bar_text))
+ iter = self.liststore.append((cardName, sizeFiles, progress, progressBarText))
+
+ 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)
- def update_device(self, process_id, total_size_files):
+ def updateCard(self, thread_id, totalSizeFiles):
"""
Updates the size of the photos and videos on the device, displayed to the user
"""
- if process_id in self.map_process_to_row:
- iter = self._get_process_map(process_id)
- self.liststore.set_value(iter, 2, total_size_files)
+ if thread_id in self.mapThreadToRow:
+ iter = self._getThreadMap(thread_id)
+ self.liststore.set_value(iter, 1, totalSizeFiles)
else:
- logger.error("This device is unknown")
+ sys.stderr.write("FIXME: this card is unknown")
- def remove_device(self, process_id):
- if process_id in self.map_process_to_row:
- iter = self._get_process_map(process_id)
+ def removeCard(self, thread_id):
+ if thread_id in self.mapThreadToRow:
+ iter = self._getThreadMap(thread_id)
self.liststore.remove(iter)
- del self.map_process_to_row[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()
+ del self.mapThreadToRow[thread_id]
- def _set_process_map(self, process_id, iter):
+ def _setThreadMap(self, thread_id, iter):
"""
convert the temporary iter into a tree reference, which is
permanent
"""
path = self.liststore.get_path(iter)
- treerowref = gtk.TreeRowReference(self.liststore, path)
- self.map_process_to_row[process_id] = treerowref
+ treerowRef = gtk.TreeRowReference(self.liststore, path)
+ self.mapThreadToRow[thread_id] = treerowRef
- def _get_process_map(self, process_id):
+ def _getThreadMap(self, thread_id):
"""
- return the tree iter for this process
+ return the tree iter for this thread
"""
- if process_id in self.map_process_to_row:
- treerowref = self.map_process_to_row[process_id]
- path = treerowref.get_path()
+ if thread_id in self.mapThreadToRow:
+ treerowRef = self.mapThreadToRow[thread_id]
+ path = treerowRef.get_path()
iter = self.liststore.get_iter(path)
return iter
else:
return None
- def update_progress(self, scan_pid, percent_complete, progress_bar_text, bytes_downloaded):
+ def updateProgress(self, thread_id, percentComplete, progressBarText, bytesDownloaded):
- iter = self._get_process_map(scan_pid)
+ iter = self._getThreadMap(thread_id)
if iter:
- if percent_complete:
- self.liststore.set_value(iter, 3, percent_complete)
- if progress_bar_text:
- self.liststore.set_value(iter, 4, progress_bar_text)
- if percent_complete or bytes_downloaded:
- pass
- #~ logger.info("Implement update overall progress")
+ 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)
+
+ 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 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
+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
- self.image_area_size = 100
- self.text_area_size = 30
- self.padding = 6
- self.checkbutton_height = checkbutton_height
- self.icon_width = 20
+ 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_set_property(self, pspec, value):
- setattr(self, pspec.name, value)
+ self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg'))
- def do_get_property(self, pspec):
- return getattr(self, pspec.name)
-
- def do_render(self, window, widget, background_area, cell_area, expose_area, flags):
-
- cairo_context = window.cairo_create()
-
- x = cell_area.x
- y = cell_area.y + self.checkbutton_height - 8
- w = cell_area.width
- h = cell_area.height
-
-
- #constrain operations to cell area, allowing for a 1 pixel border
- #either side
- #~ cairo_context.rectangle(x-1, y-1, w+2, h+2)
- #~ cairo_context.clip()
-
- #fill in the background with dark grey
- #this ensures that a selected cell's fill does not make
- #the text impossible to read
- #~ cairo_context.rectangle(x, y, w, h)
- #~ cairo_context.set_source_rgb(0.267, 0.267, 0.267)
- #~ cairo_context.fill()
-
- #image width and height
- image_w = self.image.size[0]
- image_h = self.image.size[1]
-
- #center the image horizontally
- #bottom align vertically
- #top left and right corners for the image:
- image_x = x + ((w - image_w) / 2)
- image_y = y + self.image_area_size - image_h
+ 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)
- #convert PIL image to format suitable for cairo
- image = create_cairo_image_surface(self.image, image_w, image_h)
+ secondary_label = gtk.Label()
+ secondary_label.set_text(secondary_msg)
+ secondary_label.set_line_wrap(True)
+ secondary_label.set_alignment(0, 0.5)
- # draw a light grey border of 1px around the image
- cairo_context.set_source_rgb(0.66, 0.66, 0.66) #light grey, #a9a9a9
- cairo_context.set_line_width(1)
- cairo_context.rectangle(image_x-.5, image_y-.5, image_w+1, image_h+1)
- cairo_context.stroke()
-
- # draw a thin border around each cell
- # ouch - nasty hardcoding :(
- #~ cairo_context.set_source_rgb(0.33, 0.33, 0.33)
- #~ cairo_context.rectangle(x-6.5, y-9.5, w+14, h+31)
- #~ cairo_context.stroke()
+ self.show_again_checkbutton = gtk.CheckButton(_('_Show this message again'), True)
+ self.show_again_checkbutton.set_active(True)
- #place the image
- cairo_context.set_source_surface(image, image_x, image_y)
- cairo_context.paint()
-
- #text
- context = pangocairo.CairoContext(cairo_context)
+ 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)
- 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()
+ self.set_default_response(gtk.RESPONSE_OK)
+
+ self.set_transient_for(parent_window)
+ self.show_all()
- layout = context.create_layout()
-
- width = text_w * pango.SCALE
- layout.set_width(width)
-
- layout.set_alignment(pango.ALIGN_CENTER)
- layout.set_ellipsize(pango.ELLIPSIZE_END)
+ self.connect('response', self.on_response)
- #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)
+ def on_response(self, device_dialog, response):
+ show_again = self.show_again_checkbutton.get_active()
+ self.postChoiceCB(self, show_again)
- context.move_to(text_x, text_y)
- context.show_layout(layout)
+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)
- #status
- cairo_context.set_source_pixbuf(self.status, x, y + self.image_area_size + 10)
- cairo_context.paint()
+ self.set_border_width(6)
+ self.set_has_separator(False)
- def do_get_size(self, widget, cell_area):
- #~ return (0, 0, self.image_area_size, self.image_area_size + self.checkbutton_height + 10)
- return (0, 0, self.image_area_size, self.image_area_size + self.text_area_size - self.checkbutton_height + 4)
+ 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'))
-
-gobject.type_register(ThumbnailCellRenderer)
-
-
-class ThumbnailDisplay(gtk.IconView):
- def __init__(self, parent_app):
- gtk.IconView.__init__(self)
- self.rapid_app = parent_app
+ prompt_hbox = gtk.HBox()
- self.batch_size = 10
+ 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)
+
+ self.set_border_width(6)
+ self.set_has_separator(False)
- self.thumbnail_manager = ThumbnailManager(self.thumbnail_results, self.batch_size)
- self.preview_manager = PreviewManager(self.preview_results)
+ self.set_default_response(gtk.RESPONSE_OK)
+
+
+ self.set_transient_for(parent_window)
+ self.show_all()
+
- self.treerow_index = {}
- self.process_index = {}
+ self.connect('response', self.on_response)
- self.rpd_files = {}
+ def on_response(self, device_dialog, response):
+ userSelected = response == gtk.RESPONSE_OK
+ self.postChoiceCB(self, userSelected)
- self.total_files = 0
- self.thumbnails_generated = 0
- self.thumbnails = {}
- self.previews = {}
- self.previews_being_fetched = set()
+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.stock_photo_thumbnails = tn.PhotoIcons()
- self.stock_video_thumbnails = tn.VideoIcons()
+ self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg'))
+ self.postJobCodeEntryCB = postJobCodeEntryCB
+ self.autoStart = autoStart
+ self.downloadSelected = downloadSelected
- 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.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.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
- )
+ 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.set_border_width(6)
+ self.set_has_separator(False)
- self.clear()
- self.set_model(self.liststore)
+ # make entry box have entry completion
+ self.entry = self.combobox.child
- checkbutton = gtk.CellRendererToggle()
- checkbutton.set_radio(False)
- checkbutton.props.activatable = True
- checkbutton.props.xalign = 0.0
- checkbutton.connect('toggled', self.on_checkbutton_toggled)
- self.pack_end(checkbutton, expand=False)
- self.add_attribute(checkbutton, "active", 1)
- self.add_attribute(checkbutton, "visible", 6)
+ 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)
- checkbutton_size = checkbutton.get_size(self, None)
- checkbutton_height = checkbutton_size[3]
- checkbutton_width = checkbutton_size[2]
+ # 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)
- #~ status_icon = gtk.CellRendererPixbuf()
- #~ self.pack_start(status_icon, expand=False)
- #~ self.add_attribute(status_icon, "pixbuf", self.STATUS_ICON_COL)
+ self.vbox.pack_start(task_hbox, False, False, padding = 6)
+ self.vbox.pack_start(self.job_code_hbox, False, False, padding=12)
- 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)
+ 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)
+ def get_job_code(self):
+ return self.combobox.child.get_text()
- #set the background color to a darkish grey
- self.modify_base(gtk.STATE_NORMAL, gtk.gdk.Color('#444444'))
-
- self.set_spacing(0)
- #~ self.set_column_spacing(0)
- self.set_row_spacing(5)
- #~ self.set_row_spacing(0)
- self.set_margin(25)
+ 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):
+
+ self.parentApp = parentApp
+ self.rapidApp = parentApp.parentApp
- self._setup_icons()
+ 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)
+ gtk.TreeView.__init__(self, self.liststore)
+
+ 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()
- self.connect('item-activated', self.on_item_activated)
+ # 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.user_has_clicked_header = False
- def _setup_icons(self):
# icons to be displayed in status column
- size = 16
- # standard icons
- failed = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU)
- self.download_failed_icon = failed.scale_simple(size, size, gtk.gdk.INTERP_HYPER)
- error = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU)
- self.error_icon = error.scale_simple(size, size, gtk.gdk.INTERP_HYPER)
- warning = self.render_icon(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_MENU)
- self.warning_icon = warning.scale_simple(size, size, gtk.gdk.INTERP_HYPER)
-
- # Rapid Photo Downloader specific icons
- self.downloaded_icon = gtk.gdk.pixbuf_new_from_file_at_size(
- paths.share_dir('glade3/rapid-photo-downloader-downloaded.svg'),
- size, size)
- self.download_pending_icon = gtk.gdk.pixbuf_new_from_file_at_size(
- paths.share_dir('glade3/rapid-photo-downloader-download-pending.svg'),
- 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)
+ 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)
# 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_status_icon(self, status):
+
+ 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):
"""
Returns the correct icon, based on the status
"""
@@ -499,7 +3679,10 @@ class ThumbnailDisplay(gtk.IconView):
elif status == STATUS_DOWNLOADED:
status_icon = self.downloaded_icon
elif status == STATUS_NOT_DOWNLOADED:
- status_icon = self.not_downloaded_icon
+ if preview:
+ status_icon = self.not_downloaded_icon_tick
+ else:
+ 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]:
@@ -507,1196 +3690,1464 @@ class ThumbnailDisplay(gtk.IconView):
elif status == STATUS_DOWNLOAD_PENDING:
status_icon = self.download_pending_icon
else:
- logger.critical("FIXME: unknown status: %s", status)
+ sys.stderr.write("FIXME: unknown status: %s\n" % status)
status_icon = self.not_downloaded_icon
- return status_icon
-
- def sort_by_timestamp(self):
- self.liststore.set_sort_column_id(self.TIMESTAMP_COL, gtk.SORT_ASCENDING)
+ 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
- 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()
+ If selected_only is True, then only those from the selected
+ rows will be returned.
- 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)
+ 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 add_file(self, rpd_file):
+ 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)
- thumbnail_icon = self.get_stock_icon(rpd_file.file_type)
- unique_id = rpd_file.unique_id
- scan_pid = rpd_file.scan_pid
+ 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)
- timestamp = int(rpd_file.modification_time)
-
- iter = self.liststore.append((thumbnail_icon,
- True,
- unique_id,
- rpd_file.display_name,
- timestamp,
- rpd_file.file_type,
- True,
- STATUS_NOT_DOWNLOADED,
- self.not_downloaded_icon
- ))
-
- path = self.liststore.get_path(iter)
- treerowref = gtk.TreeRowReference(self.liststore, path)
- if scan_pid in self.process_index:
- self.process_index[scan_pid].append(unique_id)
+ if mediaFile.isImage:
+ type_icon = self.icon_photo
else:
- self.process_index[scan_pid] = [unique_id,]
-
- self.treerow_index[unique_id] = treerowref
- self.rpd_files[unique_id] = rpd_file
+ 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)
- self.total_files += 1
-
- def get_sample_file(self, file_type):
- """Returns an rpd_file for of a given file type, or None if it does
- not exist"""
- for unique_id, rpd_file in self.rpd_files.iteritems():
- if rpd_file.file_type == file_type:
- if rpd_file.status <> STATUS_CANNOT_DOWNLOAD:
- return rpd_file
-
- return None
-
- def get_unique_id_from_iter(self, iter):
- return self.liststore.get_value(iter, 2)
+ 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)
- def get_iter_from_unique_id(self, unique_id):
- treerowref = self.treerow_index[unique_id]
- path = treerowref.get_path()
- return self.liststore.get_iter(path)
-
- def on_item_activated(self, iconview, path):
+ def 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
"""
- iter = self.liststore.get_iter(path)
- self.show_preview(iter=iter)
- self.advance_get_preview_image(iter)
-
+ 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
- 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
+ def update_download_selected_button(self):
+ """
+ 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)
- unique_id = self.get_unique_id_from_iter(iter)
+ #update button text
+ no_available_for_download, threads = self.no_selected_rows_available_for_download()
- rpd_file = self.rpd_files[unique_id]
-
- if unique_id in self.previews:
- preview_image = self.previews[unique_id]
+ 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:
- # 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)
+ #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)
+ 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)
- def _get_next_iter(self, iter):
- iter = self.liststore.iter_next(iter)
- if iter is None:
+ def clear_all(self, thread_id = None):
+ if thread_id is None:
+ self.liststore.clear()
+ self.show_preview(None)
+ else:
iter = self.liststore.get_iter_first()
- return iter
+ 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)
+
+ def refreshSampleDownloadFolders(self, thread_id = None):
+ """
+ Refreshes the download folder 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)
+
+ If thread_id is specified, will only update rows with that thread
+ """
+ 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
+
+ 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):
+ """
+ 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)
- def _get_prev_iter(self, iter):
- row = self.liststore.get_path(iter)[0]
- if row == 0:
- row = len(self.liststore)-1
+ If thread_id is specified, will only update rows with that thread
+ """
+ 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
+
+ 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
else:
- row -= 1
- iter = self.liststore.get_iter(row)
- return iter
+ fallback_date = mediaFile.modificationTime
+ subfolderPrefsFactory = self.videoSubfolderPrefsFactory
+ renamePrefsFactory = self.videoRenamePrefsFactory
+ nameUsesJobCode = self.videoRenameUsesJobCode
+ subfolderUsesJobCode = self.videoSubfolderUsesJobCode
+
+ 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 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)
+
+ 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):
- # cache next image
- self.advance_get_preview_image(iter, prev=False, next=True)
+ 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('')
+
+ 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
- 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)
+
+ elif not self.suspend_previews:
+ mediaFile = self.get_mediaFile(iter)
- # cache next image
- self.advance_get_preview_image(iter, prev=True, next=False)
-
+ self.previewed_file_treerowref = mediaFile.treerowref
- 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))
+ self.parentApp.set_base_preview_image(mediaFile.thumbnail)
+ thumbnail = self.parentApp.scaledPreviewImage()
+
+ self.parentApp.preview_image.set_from_pixbuf(thumbnail)
- if prev:
- prev_iter = self._get_prev_iter(iter)
- unique_ids.append(self.get_unique_id_from_iter(prev_iter))
+ 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 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)
+ 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)
+
+ 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)
+ 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
- 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
+ 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:
- row[self.SELECTED_COL] = check_all
- self.rapid_app.set_download_action_sensitivity()
+ selection.unselect_iter(iter)
+ iter = self.liststore.iter_next(iter)
- def files_are_checked_to_download(self):
+ 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):
"""
- Returns True if there is any file that the user has indicated they
- intend to download, else returns False.
+ if display is true, the column will be shown
+ otherwise, it will not be shown
"""
- 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
+ self.filename_column.set_visible(display)
- def get_files_checked_for_download(self):
+ def display_size_column(self, display):
+ self.size_column.set_visible(display)
+
+ 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 display_device_column(self, display):
+ self.device_column.set_visible(display)
+
+ def apply_job_code(self, job_code, overwrite=True, to_all_rows=False, thread_id=None):
"""
- Returns a dict of scan ids and associated files the user has indicated
- they want to download
+ Applies the Job code to the selected rows, or all rows if to_all_rows is True.
+
+ If overwrite is True, then it will overwrite any existing job code.
"""
- files = dict()
- 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)
+
+ def _apply_job_code():
+ status = self.get_status(iter)
+ if status in [STATUS_DOWNLOAD_PENDING, STATUS_WARNING, STATUS_NOT_DOWNLOADED]:
+
+ 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:
- files[scan_pid] = [rpd_file,]
+ 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
- return files
-
-
- def mark_download_pending(self, files_by_scan_pid):
+ 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 job_code_missing(self, selected_only):
"""
- Sets status to download pending and updates thumbnails display
+ 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.
"""
- 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)
- self.liststore.set_value(iter, self.CHECKBUTTON_VISIBLE_COL, False)
- self.liststore.set_value(iter, self.SELECTED_COL, False)
- self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, STATUS_DOWNLOAD_PENDING)
- icon = self.get_status_icon(STATUS_DOWNLOAD_PENDING)
- self.liststore.set_value(iter, self.STATUS_ICON_COL, icon)
-
- def select_image(self, unique_id):
- iter = self.get_iter_from_unique_id(unique_id)
- path = self.liststore.get_path(iter)
- self.select_path(path)
- self.scroll_to_path(path, use_align=False, row_align=0.5, col_align=0.5)
- def get_stock_icon(self, file_type):
- if file_type == rpdfile.FILE_TYPE_PHOTO:
- return self.stock_photo_thumbnails.stock_thumbnail_image_icon
+ 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:
- return self.stock_video_thumbnails.stock_thumbnail_image_icon
-
- def update_status_post_download(self, rpd_file):
- iter = self.get_iter_from_unique_id(rpd_file.unique_id)
- self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, rpd_file.status)
- icon = self.get_status_icon(rpd_file.status)
- self.liststore.set_value(iter, self.STATUS_ICON_COL, icon)
- self.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]]
- self.thumbnail_manager.add_task(rpd_files)
+ iter = self.liststore.get_iter_first()
+ while iter:
+ v = _job_code_missing(iter)
+ if v:
+ break
+ iter = self.liststore.iter_next(iter)
+ return v
+
- def update_thumbnail(self, thumbnail_data):
+ 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):
"""
- Takes the generated thumbnail and updates the display
+ 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)
- If the thumbnail_data includes a second image, that is used to
- update the thumbnail list using the unique_id
+ Returns a list of threads which can be downloaded
"""
- unique_id = thumbnail_data[0]
- thumbnail_icon = thumbnail_data[1]
+ threads = []
- if thumbnail_icon is not None:
- # get the thumbnail icon in PIL format
- thumbnail_icon = thumbnail_icon.get_image()
-
- treerowref = self.treerow_index[unique_id]
- path = treerowref.get_path()
+ 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))
- if thumbnail_icon:
- self.liststore.set(iter, 0, thumbnail_icon)
-
- if len(thumbnail_data) > 2:
- # get the 2nd image in PIL format
- self.thumbnails[unique_id] = thumbnail_data[2].get_image()
+ # If this row is currently previewed, then should update the preview
+ if mediaFile.treerowref == self.previewed_file_treerowref:
+ self.show_preview(iter)
-
- def thumbnail_results(self, source, condition):
- connection = self.thumbnail_manager.get_pipe(source)
+
+class SelectionVBox(gtk.VBox):
+ """
+ Dialog from which the user can select photos and videos to download
+ """
+
+
+ def __init__(self, parentApp):
+ """
+ Initialize values for log dialog, but do not display.
+ """
- conn_type, data = connection.recv()
+ gtk.VBox.__init__(self)
+ self.parentApp = parentApp
- if conn_type == rpdmp.CONN_COMPLETE:
- connection.close()
- return False
- else:
-
- for thumbnail_data in data:
- self.update_thumbnail(thumbnail_data)
-
- self.thumbnails_generated += len(data)
-
- # clear progress bar information if all thumbnails have been
- # extracted
- if self.thumbnails_generated == self.total_files:
- self.rapid_app.download_progressbar.set_fraction(0.0)
- self.rapid_app.download_progressbar.set_text('')
- else:
- self.rapid_app.download_progressbar.set_fraction(
- float(self.thumbnails_generated) / self.total_files)
-
+ tiny_screen = TINY_SCREEN
+ if tiny_screen:
+ config.max_thumbnail_size = 160
- return True
+ selection_scrolledwindow = gtk.ScrolledWindow()
+ selection_scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ selection_viewport = gtk.Viewport()
- def preview_results(self, unique_id, preview_full_size, preview_small):
- """
- Receive a full size preview image and update
- """
- self.previews_being_fetched.remove(unique_id)
- if preview_full_size:
- preview_image = preview_full_size.get_image()
- self.previews[unique_id] = preview_image
- self.rapid_app.update_preview_image(unique_id, preview_image)
-
-
- def clear_all(self, scan_pid=None, keep_downloaded_files=False):
- """
- Removes files from display and internal tracking.
- If scan_pid is not None, then only files matching that scan_pid will
- be removed. Otherwise, everything will be removed.
+ self.selection_treeview = SelectionTreeView(self)
- 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
-
-
- def add_task(self, task):
- pid = self._setup_task(task)
- logger.debug("TaskManager PID: %s", pid)
- return pid
+ 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)
- def _setup_task(self, task):
- task_results_conn, task_process_conn = Pipe(duplex=False)
- source = task_results_conn.fileno()
- self._pipes[source] = task_results_conn
- gobject.io_add_watch(source, gobject.IO_IN, self.results_callback)
+ # Preview pane
- terminate_queue = Queue()
- run_event = Event()
- run_event.set()
+ # Zoom in and out slider (make the image bigger / smaller)
- return self._initiate_task(task, task_process_conn, terminate_queue, run_event)
+ # 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)
- def _initiate_task(self, task, task_process_conn, terminate_queue, run_event):
- logger.error("Implement child class method!")
+ # 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)
-
- def processes(self):
- for i in range(len(self._processes)):
- yield self._processes[i]
-
- def start(self):
- self.paused = False
- for scan in self.processes():
- run_event = scan[2]
- if not run_event.is_set():
- run_event.set()
-
- def pause(self):
- self.paused = True
- for scan in self.processes():
- run_event = scan[2]
- if run_event.is_set():
- run_event.clear()
-
- def request_termination(self):
- """
- Send a signal to processes that they should immediately terminate
- """
- requested = False
- for p in self.processes():
- if p[0].is_alive():
- requested = True
- p[1].put(None)
- # The process might be paused: let it run
- run_event = p[2]
- if not run_event.is_set():
- run_event.set()
-
- return requested
-
- def terminate_forcefully(self):
- """
- Forcefully terminates any running processes. Use with great caution.
- No cleanup action is performed.
+ 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)
- As python essential reference (4th edition) says, if the process
- 'holds a lock or is involved with interprocess communication,
- terminating it might cause a deadlock or corrupted I/O.'
- """
- for p in self.processes():
- if p[0].is_alive():
- p[0].terminate()
+ #Preview image
+ self.base_preview_image = None # large size image used to scale down from
+ self.preview_image = gtk.Image()
-
- def get_pipe(self, source):
- return self._pipes[source]
+ self.preview_image.set_alignment(0, 0.5)
+ #leave room for thumbnail shadow
+ if DROP_SHADOW:
+ self.cacheDropShadow()
+ else:
+ self.shadow_size = 0
- def get_no_active_processes(self):
- """
- Returns how many processes are currently active, i.e. running
- """
- i = 0
- for p in self.processes():
- if p[0].is_alive():
- i += 1
- return i
-
-
-class ScanManager(TaskManager):
-
- def __init__(self, results_callback, batch_size, generate_folder,
- add_device_function):
- TaskManager.__init__(self, results_callback, batch_size)
- self.add_device_function = add_device_function
- self.generate_folder = generate_folder
+ image_size, shadow_size, offset = self._imageAndShadowSize()
- def _initiate_task(self, device, 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_process_conn, terminate_queue, run_event):
- photo_download_folder = task[0]
- video_download_folder = task[1]
- scan_pid = task[2]
- files = task[3]
+ self.preview_image.set_size_request(image_size, image_size)
- 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
+ #labels to display file information
-class ThumbnailManager(TaskManager):
+ #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)
- def _initiate_task(self, files, task_process_conn, terminate_queue, run_event):
- generator = tn.GenerateThumbnails(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 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)
+ #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()
- source = self.task_results_conn.fileno()
- gobject.io_add_watch(source, gobject.IO_IN, self.task_results)
-
+ 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)
-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()
+ 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)
- 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))
+ self.preview_device_expander.set_label_widget(device_hbox)
- 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
+ #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)
-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
+ #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()
+ 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)
-
-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)
+ 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)
- def set_image(self, image):
- self.base_image = image
+ self.preview_destination_expander.set_label_widget(destination_hbox)
+
- #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
+ #Status of the file
- self.queue_draw()
+ self.preview_status_icon = gtk.Image()
+ self.preview_status_icon.set_size_request(16,16)
+
+ 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)
- def expose(self, widget, event):
+ #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)
- cairo_context = self.window.cairo_create()
+ self.show_all()
+
+
+ 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
- x = event.area.x
- y = event.area.y
- w = event.area.width
- h = event.area.height
+ def zoom_in(self):
+ self.slider_adjustment.set_value(min([config.max_thumbnail_size, int(self.slider_adjustment.get_value()) + config.THUMBNAIL_INCREMENT]))
- #constrain operations to event area
- cairo_context.rectangle(x, y, w, h)
- cairo_context.clip_preserve()
+ 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)
- #set background color, if needed
- if self.bg_color:
- cairo_context.set_source_rgb(*self.bg_color)
- cairo_context.fill_preserve()
+ 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()
- if not self.base_image:
- return False
-
- frame_aspect = float(w) / h
-
- if frame_aspect > self.base_image_aspect:
- # Frame is wider than image
- height = h
- width = int(height * self.base_image_aspect)
else:
- # Frame is taller than image
- width = w
- height = int(width / self.base_image_aspect)
+ self.preview_destination_expander.hide()
+ self.preview_device_expander.hide()
- #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))
+ 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):
+ """
+ Generate a scaled version of the preview image
+ """
+ size = int(self.slider_adjustment.get_value())
+ if not self.base_preview_image:
+ return None
else:
- logger.debug("Downsizing image")
- tn.downsize_pil(pil_image, (width, height))
-
- #image width and height
- image_w = pil_image.size[0]
- image_h = pil_image.size[1]
-
- #center the image horizontally and vertically
- #top left and right corners for the image:
- image_x = x + ((w - image_w) / 2)
- image_y = y + ((h - image_h) / 2)
-
- image = create_cairo_image_surface(pil_image, image_w, image_h)
- cairo_context.set_source_surface(image, image_x, image_y)
- cairo_context.paint()
-
- return False
-
+ 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
+ If user is not using job codes in their file or subfolder names
+ then do not prompt for it
+ """
-class PreviewImage:
+ 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 __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 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('')
- self.base_preview_image = None # large size image used to scale down from
- self.current_preview_size = (0,0)
- self.preview_image_size_limit = (0,0)
-
- self.unique_id = None
+
+ def 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
+ """
- def set_preview_image(self, unique_id, pil_image, include_checkbutton_visible=None,
- checked=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.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
+ self.job_code_chosen(widget.get_text())
- def update_preview_image(self, unique_id, pil_image):
- if unique_id == self.unique_id:
- self.set_preview_image(unique_id, pil_image)
-
-
+ 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)
+
-class RapidApp(dbus.service.Object):
+class LogDialog(gnomeglade.Component):
"""
- The main Rapid Photo Downloader application class.
-
- Contains functionality for main program window, and directs all other
- processes.
+ Displays a log of errors, warnings or other information to the user
"""
-
- def __init__(self, bus, path, name, taskserver=None):
-
- dbus.service.Object.__init__ (self, bus, path, name)
- self.running = False
+
+ def __init__(self, parentApp):
+ """
+ Initialize values for log dialog, but do not display.
+ """
- self.taskserver = taskserver
+ gnomeglade.Component.__init__(self,
+ paths.share_dir(config.GLADE_FILE),
+ "logdialog")
+
- # Setup program preferences, and set callback for when they change
- self._init_prefs()
+ self.widget.connect("delete-event", self.hide_window)
- # Initialize widgets in the main window, and variables that point to them
- self._init_widgets()
+ self.parentApp = parentApp
+ self.log_textview.set_cursor_visible(False)
+ self.textbuffer = self.log_textview.get_buffer()
- # Initialize job code handling
- self._init_job_code()
+ 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)
- # Remember the window size from the last time the program was run, or
- # set a default size
- self._set_window_size()
+ 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()
- # Setup various widgets
- self._setup_buttons()
- self._setup_error_icons()
- self._setup_icons()
+ 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)
- # Show the main window
- self.rapidapp.show()
-
- # Check program preferences - don't allow auto start if there is a problem
- prefs_valid = prefsrapid.check_prefs_for_validity(self.prefs)
- do_not_allow_auto_start = prefs_valid
-
- # 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()
+ iter = self.textbuffer.get_end_iter()
+ self.textbuffer.insert(iter, "\n")
- # Setup devices from which to download from and backup to
- self.setup_devices(on_startup=True, on_preference_change=False,
- do_not_allow_auto_start=do_not_allow_auto_start)
+ # move viewport to display the latest message
+ adjustment = self.log_scrolledwindow.get_vadjustment()
+ adjustment.set_value(adjustment.upper)
- # Ensure the device collection scrolled window is not too small
- self._set_device_collection_size()
- #~ preferencesdialog.PreferencesDialog(self)
-
- def on_rapidapp_destroy(self, widget, data=None):
+ 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
- self._terminate_processes(terminate_file_copies = True)
+ def hide_window(self, window, event):
+ window.hide()
+ return True
- # save window and component sizes
- self.prefs.vpaned_pos = self.main_vpaned.get_position()
- x, y, width, height = self.rapidapp.get_allocation()
- self.prefs.main_window_size_x = width
- self.prefs.main_window_size_y = height
-
- self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker)
-
- gtk.main_quit()
-
- def _terminate_processes(self, terminate_file_copies=False):
-
- # FIXME: need more fine grained tuning here - must cancel large file
- # copies midstream
- if terminate_file_copies:
- logger.info("Terminating all processes...")
-
- scan_termination_requested = self.scan_manager.request_termination()
- thumbnails_termination_requested = self.thumbnails.thumbnail_manager.request_termination()
- if terminate_file_copies:
- copy_files_termination_requested = self.copy_files_manager.request_termination()
- else:
- copy_files_termination_requested = False
-
- if scan_termination_requested or thumbnails_termination_requested:
- time.sleep(1)
- if (self.scan_manager.get_no_active_processes() > 0 or
- self.thumbnails.thumbnail_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()
-
- if terminate_file_copies and copy_files_termination_requested:
- time.sleep(1)
- self.copy_files_manager.terminate_forcefully()
-
- if terminate_file_copies:
- self._clean_all_temp_dirs()
-
- # # #
- # Events and tasks related to displaying preview images and thumbnails
- # # #
-
- def on_download_this_checkbutton_toggled(self, checkbutton):
- value = checkbutton.get_active()
- self.thumbnails.set_selected(self.preview_image.unique_id, value)
- self.set_download_action_sensitivity()
-
- def on_preview_eventbox_button_press_event(self, widget, event):
-
- if event.type == gtk.gdk._2BUTTON_PRESS and event.button == 1:
- self.show_thumbnails()
-
- def on_show_thumbnails_action_activate(self, action):
- logger.debug("on_show_thumbnails_action_activate")
- self.show_thumbnails()
-
- def on_show_image_action_activate(self, action):
- logger.debug("on_show_image_action_activate")
- self.thumbnails.show_preview()
-
- def on_check_all_action_activate(self, action):
- self.thumbnails.check_all(check_all=True)
-
- def on_uncheck_all_action_activate(self, action):
- self.thumbnails.check_all(check_all=False)
-
- def on_check_all_photos_action_activate(self, action):
- self.thumbnails.check_all(check_all=True,
- file_type=rpdfile.FILE_TYPE_PHOTO)
-
- def on_check_all_videos_action_activate(self, action):
- self.thumbnails.check_all(check_all=True,
- file_type=rpdfile.FILE_TYPE_VIDEO)
-
- def on_quit_action_activate(self, action):
- self.on_rapidapp_destroy(widget=self.rapidapp, data=None)
-
- def on_refresh_action_activate(self, action):
- self.setup_devices(on_startup=False, on_preference_change=False,
- do_not_allow_auto_start=True)
-
- def on_get_help_action_activate(self, action):
- webbrowser.open("http://www.damonlynch.net/rapid/help.html")
-
- def on_about_action_activate(self, action):
- self.about.set_property("name", PROGRAM_NAME)
- self.about.set_property("version", utilities.human_readable_version(
- __version__))
- self.about.run()
- self.about.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)
-
- def update_preview_image(self, unique_id, image):
- self.preview_image.update_preview_image(unique_id, image)
-
- def show_thumbnails(self):
- logger.debug("Switching to thumbnails display")
- self.main_notebook.set_current_page(0)
- self.thumbnails.select_image(self.preview_image.unique_id)
- self.next_image_action.set_sensitive(False)
- self.prev_image_action.set_sensitive(False)
+
+class RapidApp(gnomeglade.GnomeApp, dbus.service.Object):
+ def __init__(self, bus, path, name):
+ dbus.service.Object.__init__ (self, bus, path, name)
+ self.running = False
- def on_next_image_action_activate(self, action):
- if self.preview_image.unique_id is not None:
- self.thumbnails.show_next_image(self.preview_image.unique_id)
+ gladefile = paths.share_dir(config.GLADE_FILE)
+
+ gnomeglade.GnomeApp.__init__(self, "rapid", __version__, gladefile, "rapidapp")
- 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)
+ # notifications
+ self.displayDownloadSummaryNotification = False
+ self.initPyNotify()
- 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.prefs = RapidPreferences()
+ self.prefs.notify_add(self.on_preference_changed)
+
+ self.testing = False
+ if self.testing:
+ self.setTestingEnv()
+
+# sys.exit(0)
- # # #
- # 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)
-
+ # 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)
+ else:
+ # set a default size
+ self.rapidapp.set_default_size(650, 650)
- def setup_devices(self, on_startup, on_preference_change, do_not_allow_auto_start):
- """
+ 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()
- Setup devices from which to download from and backup to
+ self._setupIcons()
- Sets up volumes for downloading from and backing up to
+ # 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)
- on_startup should be True if the program is still starting,
- i.e. this is being called from the program's initialization.
+ self.checkIfFirstTimeProgramEverRun()
+
+ displayPreferences = self.checkForUpgrade(__version__)
+ self.prefs.program_version = __version__
- on_preference_change should be True if this is being called as the
- result of a preference being changed
+ self.timeRemaining = TimeRemaining()
+ self._resetDownloadInfo()
+ self.statusbar_context_id = self.rapid_statusbar.get_context_id("progress")
- Removes any image media that are currently not downloaded,
- or finished downloading
- """
+ # hide display of warning and error symbols in the taskbar until they are needed
+ self.error_image.hide()
+ self.warning_image.hide()
+ self.warning_vseparator.hide()
- if self.using_volume_monitor():
- self.start_volume_monitor()
+ if not displayPreferences:
+ displayPreferences = not self.checkPreferencesOnStartup()
+ # display download information using threads
+ global media_collection_treeview, log_dialog
+ global workers
- self.clear_non_running_downloads()
-
- mounts = []
- self.backup_devices = {}
+ #track files that should have a suffix added to them
+ global duplicate_files
- # Clear download statistics and tracking
- # FIXME
+ #track files that have been downloaded in this session
+ global downloaded_files
- 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))
-
+ # control sequence numbers and letters
+ global sequences
- 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)
+ # whether we need to prompt for a job code
+ global need_job_code_for_renaming
- 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
+ duplicate_files = {}
+ downloaded_files = DownloadedFiles()
- # Display amount of free space in a status bar message
- self.display_free_space()
+ self.download_start_time = None
- if do_not_allow_auto_start:
- self.auto_start_is_on = False
- else:
- self.auto_start_is_on = ((not on_preference_change) and
- ((self.prefs.auto_download_at_startup and
- on_startup) or
- (self.prefs.auto_download_upon_device_insertion and
- not on_startup)))
+ downloadsToday = self.prefs.getAndMaybeResetDownloadsToday()
+ sequences = rn.Sequences(downloadsToday, self.prefs.stored_sequence_no)
+
+ self.downloadStats = DownloadStats()
+ # set the number of seconds gap with which to measure download time remaing
+ self.downloadTimeGap = 3
+
+ #locks for threadsafe file downloading and stats gathering
+ self.fileRenameLock = Lock()
+ self.fileSequenceLock = Lock()
+ self.statsLock = Lock()
+ self.downloadedFilesLock = Lock()
- self.testing_auto_exit = False
- self.testing_auto_exit_trip = len(mounts)
- self.testing_auto_exit_trip_counter = 0
+ # log window, in dialog format
+ # used for displaying download information to the user
+ log_dialog = LogDialog(self)
- 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
+
+ self.volumeMonitor = None
+ if self.usingVolumeMonitor():
+ self.startVolumeMonitor()
- def get_use_device(self, device):
- """ Prompt user whether or not to download from this device """
+ # 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
- logger.info("Prompting whether to use %s", device.get_name())
- d = dv.UseDeviceDialog(self.rapidapp, device, self.got_use_device)
+ # flag to indicate the user changes some preferences and the display
+ # of sample names and subfolders needs to be refreshed
+ self.refreshGeneratedSampleSubfolderAndName = False
- 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()
+ # 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
- path = device.get_path()
+ # 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
- 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
-
- 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
+ # flag to indicate that the preferences dialog window is being
+ # displayed to the user
+ self.preferencesDialogDisplayed = False
+
+ # set up tree view display to display image devices and download status
+ media_collection_treeview = MediaTreeView(self)
- def check_if_backup_mount(self, path):
- """
- Checks to see if backups are enabled and path represents a valid backup location
+ self.media_collection_vbox.pack_start(media_collection_treeview)
- 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 using_volume_monitor(self):
- """
- Returns True if programs needs to use gio volume monitor
- """
+ #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)
- 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
- """
+ self.backupVolumes = {}
+ #Help button and download buttons
+ self._setupDownloadbuttons()
+
+ #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)
+
- if mount.is_shadowed():
- # ignore this type of mount
- return
-
- path = mount.get_root().get_path()
+ # menus
- 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})
+ #preview panes
+ self.menu_display_selection.set_active(self.prefs.display_selection)
+ self.menu_preview_folders.set_active(self.prefs.display_preview_folders)
+
+ #preview columns in pane
+ if not DOWNLOAD_VIDEO:
+ self.menu_type_column.set_active(False)
+ self.menu_type_column.set_sensitive(False)
else:
- 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
- self.display_free_space()
+ 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.menu_clear.set_sensitive(False)
- elif self.prefs.device_autodetection and (dv.is_DCIM_device(path) or
- self.search_for_PSD()):
-
- 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
+ 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)
+
+ #job code initialization
+ self.last_chosen_job_code = None
+ self.prompting_for_job_code = False
+
+ #check to see if the download folder exists and is writable
+ displayPreferences_2 = not self.checkDownloadPathOnStartup()
+ displayPreferences = displayPreferences or displayPreferences_2
- def on_mount_removed(self, vmonitor, mount):
- """
- callback run when gio indicates a new volume
- has been mounted
- """
+ if self.prefs.device_autodetection == False:
+ displayPreferences_2 = not self.checkImageDevicePathOnStartup()
+ displayPreferences = displayPreferences or displayPreferences_2
- path = mount.get_root().get_path()
+ #setup download and backup mediums, initiating scans
+ self.setupAvailableImageAndBackupMedia(onStartup=True, onPreferenceChange=False, doNotAllowAutoStart = displayPreferences)
- # 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
+ #adjust viewport size for displaying media
+ #this is important because the code in MediaTreeView.addCard() is inaccurate at program startup
- 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
-
- #~ if scan_pid in self.download_active_by_scan_pid:
- #~ self._clean_temp_dirs_for_scan_pid(scan_pid)
- self.thumbnails.clear_all(scan_pid = scan_pid,
- keep_downloaded_files = True)
- self.device_collection.remove_device(scan_pid)
-
-
-
- # remove backup volumes
- elif path in self.backup_devices:
- del self.backup_devices[path]
- self.display_free_space()
-
- # may need to disable download button and menu
- self.set_download_action_sensitivity()
-
- def clear_non_running_downloads(self):
- """
- Clears the display of downloads that are currently not running
- """
+ 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)
- # Stop any processes currently scanning or creating thumbnails
- self._terminate_processes(terminate_file_copies=False)
+ self.download_button.grab_default()
+ # for some reason, the grab focus command is not working... unsure why
+ self.download_button.grab_focus()
- # 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 displayPreferences:
+ PreferencesDialog(self)
-
+
+ @dbus.service.method (config.DBUS_NAME,
+ in_signature='', out_signature='b')
+ def is_running (self):
+ return self.running
- # # #
- # Download and help buttons, and menu items
- # # #
+ @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
+
+ 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 on_download_action_activate(self, action):
+ def displayFreeSpace(self):
"""
- Called when a download is activated
+ Displays the amount of space free on the filesystem the files will be downloaded to.
+ Also displays backup volumes / path being used.
"""
+ 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}
- if self.copy_files_manager.paused:
- logger.debug("Download resumed")
- self.resume_download()
- else:
- logger.debug("Download activated")
- if self.download_action_is_download:
- self.start_download()
+ 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:
- self.pause_download()
-
+ msg2 = self.displayBackupVolumes()
+
+ if msg:
+ msg = _("%(freespace)s. %(backuppaths)s.") % {'freespace': msg, 'backuppaths': msg2}
+ else:
+ msg = msg2
+
+ self.rapid_statusbar.push(self.statusbar_context_id, msg)
- def on_help_action_activate(self, action):
- webbrowser.open("http://www.damonlynch.net/rapid/documentation")
-
- def on_preferences_action_activate(self, action):
-
- preferencesdialog.PreferencesDialog(self)
-
- def set_download_action_sensitivity(self):
- """
- Sets sensitivity of Download action to enable or disable it
+ 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})
+
+ 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
- Affects download button and menu item
- """
- if not self.download_is_occurring():
- sensitivity = False
- if self.scan_manager.get_no_active_processes() == 0:
- if self.thumbnails.files_are_checked_to_download():
- sensitivity = True
+ def checkDownloadPathOnStartup(self):
+ if DOWNLOAD_VIDEO:
+ paths = ((self.prefs.download_folder, _('Photo')), (self.prefs.video_download_folder, _('Video')))
+ 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)
- self.download_action.set_sensitive(sensitivity)
+ 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")
+ else:
+ title = _("Problem with Download Folders")
- 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
+ misc.run_dialog(title, msg,
+ self,
+ gtk.MESSAGE_ERROR)
+ return False
else:
- self.download_action.set_label(_("Pause"))
- self.download_action_is_download = False
-
- # # #
- # Job codes
- # # #
-
-
- def _init_job_code(self):
- self.job_code = None
- self.need_job_code_for_naming = self.prefs.any_pref_uses_job_code()
+ return True
- def assign_job_code(self, code):
+ 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 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 assignJobCode(self, code):
""" assign job code (which may be empty) to global variable and update user preferences
Update preferences only if code is not empty. Do not duplicate job code.
"""
- # FIXME
- #~ global job_code
+ global job_code
if code == None:
code = ''
job_code = code
@@ -1712,921 +5163,1301 @@ class RapidApp(dbus.service.Object):
jcs.remove(code)
self.prefs.job_codes = [code] + jcs
-
- def _get_job_code(self, post_job_code_entry_callback, autoStart, downloadSelected):
+
+
+ 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):
""" prompt for a job code """
if not self.prompting_for_job_code:
- logger.debug("Prompting for Job Code")
+ cmd_line(_("Prompting for Job Code"))
self.prompting_for_job_code = True
- j = preferencesdialog.JobCodeDialog(parent_window = self.rapidapp,
- job_codes = self.prefs.job_codes,
- default_job_code = self.last_chosen_job_code,
- post_job_code_entry_callback=post_job_code_entry_callback,
- entry_only = False)
+ j = JobCodeDialog(self.widget, self.prefs.job_codes, self.last_chosen_job_code, postJobCodeEntryCB, autoStart, downloadSelected, False)
else:
- logger.debug("Already prompting for Job Code, do not prompt again")
+ cmd_line(_("Already prompting for Job Code, do not prompt again"))
- def get_job_code(self):
- self._get_job_code(self.got_job_code)
+ def getJobCode(self, autoStart=True, downloadSelected=False):
+ """ called from the copyphotos thread, or when the user clicks one of the two download buttons"""
+
+ self._getJobCode(self.gotJobCode, autoStart, downloadSelected)
- def got_job_code(self, dialog, user_chose_code, code):
+ def gotJobCode(self, dialog, userChoseCode, code, autoStart, downloadSelected):
dialog.destroy()
self.prompting_for_job_code = False
- if user_chose_code:
- self.assign_job_code(code)
+ if userChoseCode:
+ self.assignJobCode(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:
- #~ logger.debug("Starting downloads")
- #~ self.startDownload(threads)
- #~ else:
- #~ # autostart is true
- #~ logger.debug("Starting downloads that have been waiting for a Job Code")
- #~ for w in workers.getWaitingForJobCodeWorkers():
- #~ w.startStop()
+ 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()
else:
# user cancelled
- pass
- #~ logger.debug("No Job Code entered")
- #~ for w in workers.getWaitingForJobCodeWorkers():
- #~ w.waitingForJobCode = False
- #~
- #~ if autoStart:
- #~ for w in workers.getAutoStartWorkers():
- #~ w.autoStart = 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()
+ 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)
- # Track which temporary directories are created when downloading files
- self.temp_dirs_by_scan_pid = dict()
+ 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)
- # Track which downloads are running
- self.download_active_by_scan_pid = []
+ 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)
-
-
- def start_download(self):
- """
- Start download, renaming and backup of files.
- """
+ 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)
- self.download_start_time = datetime.datetime.now()
- files_by_scan_pid = self.thumbnails.get_files_checked_for_download()
- folders_valid, invalid_dirs = self.check_download_folder_validity(files_by_scan_pid)
+ 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)
- if not folders_valid:
- if len(invalid_dirs) > 1:
- msg = _("These download folders are invalid:\n%(folder1)s\n%(folder2)s") % {
- 'folder1': invalid_dirs[0], 'folder2': invalid_dirs[1]}
- else:
- msg = _("This download folder is invalid:\n%s") % invalid_dirs[0]
- self.log_error(config.CRITICAL_ERROR, _("Download cannot proceed"),
- msg)
- else:
- self.thumbnails.mark_download_pending(files_by_scan_pid)
- for scan_pid in files_by_scan_pid:
- files = files_by_scan_pid[scan_pid]
- self.download_files(files, scan_pid)
+ 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)
- self.set_download_action_label(is_download = False)
+ def checkIfFirstTimeProgramEverRun(self):
+ """
+ if this is the first time the program has been run, then
+ might need to create default directories
+ """
+ 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.
- def pause_download(self):
+ If the version is different, then the preferences are checked to see whether they should be upgraded or not.
- self.copy_files_manager.pause()
+ returns True if program preferences window should be opened """
- # set action to display Download
- if not self.download_action_is_download:
- self.set_download_action_label(is_download = True)
-
- def resume_download(self):
- self.copy_files_manager.start()
-
- def download_files(self, files, scan_pid):
- """
- Initiate downloading and renaming of files
- """
+ displayPrefs = upgraded = False
- # 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:
- 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
+ previousVersion = self.prefs.program_version
+ if len(previousVersion) > 0:
+ # the program has been run previously for this user
+
+ pv = common.pythonifyVersion(previousVersion)
+ rv = common.pythonifyVersion(runningVersion)
- self.download_tracker.init_stats(scan_pid=scan_pid,
- bytes=self.size_files_to_be_downloaded(files),
- no_files=len(files))
+ title = PROGRAM_NAME
+ imageRename = subfolder = None
- self.download_active_by_scan_pid.append(scan_pid)
- # 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 = data
- self.download_tracker.set_total_bytes_copied(scan_pid,
- total_downloaded)
- percent_complete = self.download_tracker.get_percent_complete(scan_pid)
- self.device_collection.update_progress(scan_pid, percent_complete,
- None, None)
- elif msg_type == rpdmp.MSG_FILE:
- download_succeeded, rpd_file, download_count, temp_full_file_name = data
-
- self.download_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()
-
- self.subfolder_file_manager.rename_file_and_move_to_subfolder(
- download_succeeded,
- download_count,
- rpd_file
- )
-
+ 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
- return 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
+
+ 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."))
else:
- # Process is complete, i.e. conn_type == rpdmp.CONN_COMPLETE
- connection.close()
- return False
-
+ try:
+ if info["name"] == 'notify-osd':
+ do_not_size_icon = True
+ except:
+ pass
+
+ if do_not_size_icon:
+ self.application_icon = gtk.gdk.pixbuf_new_from_file(
+ paths.share_dir('glade3/rapid-photo-downloader.svg'))
+ else:
+ self.application_icon = gtk.gdk.pixbuf_new_from_file_at_size(
+ paths.share_dir('glade3/rapid-photo-downloader.svg'),
+ self.notification_icon_size, self.notification_icon_size)
+
- def download_is_occurring(self):
- """Returns True if a file is currently being downloaded or renamed
+ def usingVolumeMonitor(self):
+ """
+ Returns True if programs needs to use gio or gnomevfs volume monitor
"""
- return not len(self.download_active_by_scan_pid) == 0
+
+ return (self.prefs.device_autodetection or
+ (self.prefs.backup_images and
+ self.prefs.backup_device_autodetection
+ ))
+
- # # #
- # Create folder and file names for downloaded files
- # # #
+ def startVolumeMonitor(self):
+ if not self.volumeMonitor:
+ self.volumeMonitor = VMonitor(self)
- def subfolder_file_results(self, move_succeeded, rpd_file):
+ def displayBackupVolumes(self):
"""
- Handle results of subfolder creation and file renaming
+ Create a message to be displayed to the user showing which backup volumes will be used
"""
-
- scan_pid = rpd_file.scan_pid
- unique_id = rpd_file.unique_id
+ message = ''
- self.thumbnails.update_status_post_download(rpd_file)
+ 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
- # Update error log window if neccessary
- if not move_succeeded:
- self.log_error(config.SERIOUS_ERROR, rpd_file.error_title,
- rpd_file.error_msg, rpd_file.error_extra_detail)
- elif rpd_file.status == config.STATUS_DOWNLOADED_WITH_WARNING:
- self.log_error(config.WARNING, rpd_file.error_title,
- rpd_file.error_msg, rpd_file.error_extra_detail)
+ 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
- self.download_tracker.file_downloaded_increment(scan_pid)
- self._update_file_download_device_progress(scan_pid, unique_id)
-
- download_count = self.download_tracker.get_download_count_for_file(unique_id)
- if download_count == self.download_tracker.get_no_files_in_download(scan_pid):
- # Last file has been downloaded, so clean temp directory
- logger.debug("Purging temp directories")
- self._clean_temp_dirs_for_scan_pid(scan_pid)
- self.download_tracker.purge(scan_pid)
- self.download_active_by_scan_pid.remove(scan_pid)
+
+ 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()
+ else:
+ return str(type(gMount)).find('GProxyShadowMount') >= 0
+ else:
+ return False
- if not self.download_is_occurring():
- logger.debug("Download completed")
-
- 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)
-
- self.display_free_space()
-
- self.set_download_action_label(is_download=True)
- self.set_download_action_sensitivity()
+ 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
else:
- pass
- #~ logger.info("Download count: %s", download_count)
-
+ return False
+ def workerHasThisPath(self, path):
+ havePath= False
+ for w in workers.getNonFinishedWorkers():
+ if w.cardMedia.path == path:
+ havePath = True
+ break
+ return havePath
- def _update_file_download_device_progress(self, scan_pid, unique_id):
+ def on_volume_mounted(self, monitor, mount):
"""
- Increments the progress bar for an individual device
+ callback run when gnomevfs indicates a new volume
+ has been mounted
"""
- progress_bar_text = _("%(number)s of %(total)s %(filetypes)s") % \
- {'number': self.download_tracker.get_download_count_for_file(unique_id),
- 'total': self.download_tracker.get_no_files_in_download(scan_pid),
- 'filetypes': self.download_tracker.get_file_types_present(scan_pid)}
- percent_complete = self.download_tracker.get_percent_complete(scan_pid)
- self.device_collection.update_progress(scan_pid=scan_pid,
- percent_complete=percent_complete,
- progress_bar_text=progress_bar_text,
- bytes_downloaded=None)
-
- def _clean_all_temp_dirs(self):
+
+ 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):
"""
- Cleans all temp dirs if they exist
+ callback run when gnomevfs indicates a volume
+ has been unmounted
"""
- 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)
+
+ 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
+
+ # remove backup volumes
+ if path in self.backupVolumes:
+ del self.backupVolumes[path]
+ self.displayFreeSpace()
- self.temp_dirs_by_scan_pid = {}
-
+ # may need to disable download button
+ self.setDownloadButtonSensitivity()
+
- def _clean_temp_dirs_for_scan_pid(self, scan_pid):
+ def clearCompletedDownloads(self):
"""
- Deletes temp files and folders used in download
+ clears the display of completed downloads
"""
- for temp_dir in self.temp_dirs_by_scan_pid[scan_pid]:
- self._purge_dir(temp_dir)
- del self.temp_dirs_by_scan_pid[scan_pid]
- def _purge_dir(self, directory):
- """
- Deletes all files in the directory, and the directory itself.
+ for w in workers.getFinishedWorkers():
+ media_collection_treeview.removeCard(w.thread_id)
+ self.selection_vbox.selection_treeview.clear_all(w.thread_id)
+
+
+
- Does not recursively traverse any subfolders in the directory.
+ def clearNotStartedDownloads(self):
+ """
+ Clears the display of the download and instructs the thread not to run
"""
- 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())
- #~ logger.info("Deleting %s", 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)
-
+ for w in workers.getNotDownloadingWorkers():
+ media_collection_treeview.removeCard(w.thread_id)
+ workers.disableWorker(w.thread_id)
- # # #
- # Preferences
- # # #
-
- def check_prefs_on_startup(self):
+ def checkIfBackupVolume(self, path):
"""
- Checks the image & video rename, and subfolder prefs for validity.
+ Checks to see if backups are enabled and path represents a valid backup location
- Returns True if no problem, false otherwise.
+ Checks against user preferences.
"""
- prefs_ok = prefsrapid.check_prefs_for_validity(self.prefs.image_rename,
- self.prefs.subfolder,
- self.prefs.video_rename,
- self.prefs.video_subfolder)
- if not prefs_ok:
- logger.error("There is an error in the program preferences relating to file renaming and subfolder creation. Some preferences will be reset.")
- return prefs_ok
+ 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 _init_prefs(self):
- self.prefs = prefsrapid.RapidPreferences()
- self.prefs.notify_add(self.on_preference_changed)
+ def _printAutoStart(self, autoStart):
+ if autoStart:
+ cmd_line(_("Automatically start download is true") )
+ else:
+ cmd_line(_("Automatically start download is false") )
- # 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
+ def setupAvailableImageAndBackupMedia(self, onStartup, onPreferenceChange, doNotAllowAutoStart):
+ """
+ Sets up volumes for downloading from and backing up to
- # flag to indicate that the preferences dialog window is being
- # displayed to the user
- self.preferences_dialog_displayed = False
-
- # flag to indicate that the user has modified the download today
- # related values in the preferences dialog window
- self.refresh_downloads_today = False
+ 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
+ """
+
+ 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
- self.downloads_today_tracker = self.prefs.get_downloads_today_tracker()
+ self.displayFreeSpace()
+ # add each memory card / other device to the list of threads
- 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)
+ if doNotAllowAutoStart:
+ autoStart = False
else:
- logger.info("No downloads have occurred so far today")
-
- self.downloads_today_value = Value(c_int,
- self.downloads_today_tracker.get_raw_downloads_today())
- self.downloads_today_date_value = Array(c_char,
- self.downloads_today_tracker.get_raw_downloads_today_date())
- self.day_start_value = Array(c_char,
- self.downloads_today_tracker.get_raw_day_start())
- self.refresh_downloads_today_value = Value(c_bool, False)
- self.stored_sequence_value = Value(c_int, self.prefs.stored_sequence_no)
- self.uses_stored_sequence_no_value = Value(c_bool, self.prefs.any_pref_uses_stored_sequence_no())
- self.uses_session_sequece_no_value = Value(c_bool, self.prefs.any_pref_uses_session_sequece_no())
- self.uses_sequence_letter_value = Value(c_bool, self.prefs.any_pref_uses_sequence_letter_value())
+ autoStart = (not onPreferenceChange) and ((self.prefs.auto_download_at_startup and onStartup) or (self.prefs.auto_download_upon_device_insertion and not onStartup))
- self.prefs.program_version = __version__
+ self._printAutoStart(autoStart)
- 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 on_preference_changed(self, key, value):
+ shownWarning = 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):
"""
- Called when user changes the program's preferences
+ Setup the backup media
+
+ Assumptions: this is being called after the user has changed their preferences AND download media has already been setup
"""
- logger.debug("Preference change detected: %s", key)
+ 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()
+ def _setupDownloadbuttons(self):
+ self.download_hbutton_box = gtk.HButtonBox()
+ self.download_hbutton_box.set_spacing(12)
+ self.download_hbutton_box.set_homogeneous(False)
- 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()
-
- 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()
+ 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()
+ 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
- # Downloads today and stored sequence numbers are kept in shared memory,
- # so that the subfolderfile daemon process can access and modify them
+ self.startTime = time.time()
+ self.timeStatusBarUpdated = self.startTime
+
+ self.timeMark = self.startTime
+ self.sizeMark = 0
- # Note, totally ignore any changes in downloads today, as it
- # is modified in a special manner via a tracking class
-
- elif key == 'stored_sequence_no':
- if type(value) <> types.IntType:
- logger.critical("Stored sequence number value is malformed")
- else:
- self.stored_sequence_value.value = value
-
- elif key in ['image_rename', 'subfolder', 'video_rename', 'video_subfolder']:
- # Check if stored sequence no is being used
- self._check_for_sequence_value_use()
-
- #~ elif key == 'job_codes':
- #~ # update job code list in left pane
- #~ self.selection_vbox.update_job_code_combo()
+ 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()
+
- elif key in ['download_folder', 'video_download_folder']:
- self.display_free_space()
+ #start any new workers that have downloads pending
+ for i in threads:
+ workers[i].startStop()
+
+ if is_beta and verbose and False:
+ workers.printWorkerStatus()
- def post_preference_change(self):
- if self.rerun_setup_available_image_and_video_media:
- if self.using_volume_monitor():
- self.start_volume_monitor()
- logger.info("Download device settings preferences were changed.")
-
- self.thumbnails.clear_all()
- self.setup_devices(on_startup = False, on_preference_change = True, do_not_allow_auto_start = True)
- if self.main_notebook.get_current_page() == 1: # preview of file
- self.main_notebook.set_current_page(0)
+ def setDownloadStartTime(self):
+ if not self.download_start_time:
+ self.download_start_time = datetime.datetime.now()
+
+ def updateOverallProgress(self, thread_id, bytesDownloaded, percentComplete):
+ """
+ Updates progress bar and status bar text with time remaining
+ to download images
+ """
- 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.")
-
- logger.info("self.refreshBackupMedia()")
-
- 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)
+ 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)
+
+ timeRemaining = self.timeRemaining.timeRemaining()
+ if timeRemaining:
+ secs = int(timeRemaining)
+
+ if secs == 0:
+ message = ""
+ elif secs == 1:
+ message = _("About 1 second remaining")
+ elif secs < 60:
+ message = _("About %i seconds remaining") % secs
+ elif secs == 60:
+ message = _("About 1 minute remaining")
+ else:
+ # Translators: in the text '%(minutes)i:%(seconds)02i', only the : should be translated, if needed.
+ # '%(minutes)i' and '%(seconds)02i' should not be modified or left out. They are used to format and display the amount
+ # of time the download has remainging, e.g. 'About 5:36 minutes remaining'
+ message = _("About %(minutes)i:%(seconds)02i minutes remaining") % {'minutes': secs / 60, 'seconds': secs % 60}
+
+ self.rapid_statusbar.pop(self.statusbar_context_id)
+ self.rapid_statusbar.push(self.statusbar_context_id, message)
+
- # # #
- # Main app window management and setup
- # # #
+ def resetSequences(self):
+ if self.downloadComplete():
+ sequences.reset(self.prefs.getDownloadsToday(), self.prefs.stored_sequence_no)
- 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")
+ 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 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.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")
+
+ def downloadFailed(self, thread_id):
+ if workers.noDownloadingWorkers() == 0:
+ self.download_button_is_download = True
+ self._set_download_button()
+ self.setDownloadButtonSensitivity()
+
+ def downloadComplete(self):
+ return self.totalDownloadedSoFar == self.totalDownloadSize
+
+ def setDownloadButtonSensitivity(self):
+
+ isSensitive = (workers.noReadyToDownloadWorkers() > 0 and
+ workers.noScanningWorkers() == 0 and
+ self.selection_vbox.selection_treeview.rows_available_for_download()) or \
+ workers.noDownloadingWorkers() > 0
- # Only enable this action when actually displaying a preview
- self.next_image_action.set_sensitive(False)
- self.prev_image_action.set_sensitive(False)
+ 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
+ else:
+ self.download_button.props.sensitive = False
+ self.download_selected_button.props.sensitive = False
+ self.menu_download_pause.props.sensitive = False
+
+ return isSensitive
- # About dialog
- builder.add_from_file(paths.share_dir("glade3/about.ui"))
- self.about = builder.get_object("about")
- builder.connect_signals(self)
+ def on_rapidapp_destroy(self, widget):
+ """Called when the application is going to quit"""
- self.preview_image = PreviewImage(self, builder)
+ # 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()
- thumbnails_scrolledwindow = builder.get_object('thumbnails_scrolledwindow')
- self.thumbnails = ThumbnailDisplay(self)
- thumbnails_scrolledwindow.add(self.thumbnails)
-
- #collection of devices from which to download
- self.device_collection_viewport = builder.get_object("device_collection_viewport")
- self.device_collection = DeviceCollection(self)
- self.device_collection_viewport.add(self.device_collection)
+ x, y = self.rapidapp.get_size()
+ self.prefs.main_window_size_x = x
+ self.prefs.main_window_size_y = y
+
+ workers.quitAllWorkers()
+
+ self.flushevents()
- #error log window
- self.error_log = errorlog.ErrorLog(self)
+ 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
+
+
+ def on_menu_clear_activate(self, widget):
+ self.clearCompletedDownloads()
+ widget.set_sensitive(False)
- # 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 = {}
+ def on_menu_refresh_activate(self, widget):
+ self.selection_vbox.selection_treeview.clear_all()
+ self.setupAvailableImageAndBackupMedia(onStartup = False, onPreferenceChange = True, doNotAllowAutoStart = True)
- # Download action state
- self.download_action_is_download = True
+ def on_menu_report_problem_activate(self, widget):
+ webbrowser.open("https://bugs.launchpad.net/rapid")
- #job code initialization
- self.last_chosen_job_code = None
- self.prompting_for_job_code = False
+ def on_menu_get_help_online_activate(self, widget):
+ webbrowser.open("http://www.damonlynch.net/rapid/help.html")
- def _set_window_size(self):
- """
- Remember the window size from the last time the program was run, or
- set a default size
- """
-
- if self.prefs.main_window_maximized:
- self.rapidapp.maximize()
- self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH,
- config.DEFAULT_WINDOW_HEIGHT)
- elif self.prefs.main_window_size_x > 0:
- self.rapidapp.set_default_size(self.prefs.main_window_size_x, self.prefs.main_window_size_y)
- else:
- # set a default size
- self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH,
- config.DEFAULT_WINDOW_HEIGHT)
-
+ def on_menu_donate_activate(self, widget):
+ webbrowser.open("http://www.damonlynch.net/rapid/donate.html")
- def _set_device_collection_size(self):
- """
- Set the size of the device collection scrolled window widget
- """
-
+ 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)
- 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)
+ def on_menu_log_window_toggled(self, widget):
+ active = widget.get_active()
+ self.prefs.show_log_dialog = active
+ if active:
+ log_dialog.widget.show()
else:
- # don't allow the media collection to be absolutely empty
- self.device_collection_scrolledwindow.set_size_request(-1, 47)
-
-
- def on_rapidapp_window_state_event(self, widget, event):
- """ Records the window maximization state in the preferences."""
-
- if event.changed_mask & gdk.WINDOW_STATE_MAXIMIZED:
- self.prefs.main_window_maximized = event.new_window_state & gdk.WINDOW_STATE_MAXIMIZED
+ log_dialog.widget.hide()
+
+ def on_menu_display_selection_toggled(self, check_button):
+ self.prefs.display_selection = check_button.get_active()
- 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_preview_folders_toggled(self, check_button):
+ self.prefs.display_preview_folders = check_button.get_active()
- 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)
+ def on_menu_zoom_out_activate(self, widget):
+ self.selection_vbox.zoom_out()
- 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)
+ def on_menu_zoom_in_activate(self, widget):
+ self.selection_vbox.zoom_in()
- 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_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')
- 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_all_videos_activate(self, widget):
+ self.selection_vbox.selection_treeview.select_rows('videos')
- def statusbar_message(self, msg):
- self.rapid_statusbar.push(self.statusbar_context_id, msg)
-
- def statusbar_message_remove(self):
- self.rapid_statusbar.pop(self.statusbar_context_id)
+ def on_menu_select_none_activate(self, widget):
+ self.selection_vbox.selection_treeview.select_rows('none')
- def display_free_space(self):
+ 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 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):
"""
- Displays the amount of space free on the filesystem the files will be
- downloaded to.
-
- Also displays backup volumes / path being used. (NOT IMPLEMENTED YET)
+ Sets download button to appropriate state
"""
- photo_dir = self.is_valid_download_dir(path=self.prefs.download_folder, is_photo_dir=True, show_error_in_log=True)
- video_dir = self.is_valid_download_dir(path=self.prefs.video_download_folder, is_photo_dir=False, show_error_in_log=True)
- if photo_dir and video_dir:
- same_file_system = self.same_file_system(self.prefs.download_folder,
- self.prefs.video_download_folder)
- else:
- same_file_system = False
-
- dirs = []
- if photo_dir:
- dirs.append((self.prefs.download_folder, _("photos")))
- if video_dir and not same_file_system:
- dirs.append((self.prefs.video_download_folder, _("videos")))
-
- msg = ''
- if len(dirs) > 1:
- msg = ' ' + _('Free space:') + ' '
-
- for i in range(len(dirs)):
- dir_info = dirs[i]
- folder = gio.File(dir_info[0])
- file_info = folder.query_filesystem_info(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE)
- size = file_info.get_attribute_uint64(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE)
- free = format_size_for_user(bytes=size)
- if len(dirs) > 1:
- #(videos) or (photos) will be appended to the free space message displayed to the
- #user in the status bar.
- #you should only translate this if your language does not use parantheses
- file_type = _("(%(file_type)s)") % {'file_type': dir_info[1]}
-
- #Freespace available on the filesystem for downloading to
- #Displayed in status bar message on main window
- msg += _("%(free)s %(file_type)s") % {'free': free, 'file_type': file_type}
- if i == 0:
- #Inserted in the middle of the statusbar message concerning the amount of freespace
- #Used to differentiate between two different file systems
- #e.g. Free space: 21.3GB (photos); 14.7GB (videos).
- msg += _("; ")
- 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}
+ 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()
- if self.prefs.backup_images and False: #FIXME: skip this for now!
- 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.displayBackupVolumes() #FIXME
-
- if msg:
- msg = _("%(freespace)s. %(backuppaths)s.") % {'freespace': msg, 'backuppaths': msg2}
+ 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:
- msg = msg2
-
- msg = msg.rstrip()
+ self.download_button.set_label(_("_Download All"))
+ self.download_selected_button.show_all()
+
+ 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()
- self.statusbar_message(msg)
+ def on_menu_download_pause_activate(self, widget):
+ self.on_download_button_clicked(widget)
- def log_error(self, severity, problem, details, extra_detail=None):
- """
- Display error and warning messages to user in log window
+ 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):
"""
- self.error_log.add_message(severity, problem, details, extra_detail)
-
-
- def on_error_eventbox_button_press_event(self, widget, event):
- self.prefs.show_log_dialog = True
- self.error_log.widget.show()
+ Handle download button click.
+ Button is in one of three states: download all, resume, or pause.
- 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()
+ If download, a click indicates to start or resume a download run.
+ If pause, a click indicates to pause all running downloads.
+ """
+ if self.download_button_is_download:
+ if need_job_code_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()
else:
- self.error_log.widget.hide()
-
-
- # # #
- # Utility functions
- # # #
+ 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)
+
- 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
+ def on_help_button_clicked(self, widget):
+ webbrowser.open("http://www.damonlynch.net/rapid/help.html")
+
+ def on_preference_changed(self, key, value):
"""
- size = 0
- for i in range(len(files)):
- size += files[i].size
-
- return size
-
- def check_download_folder_validity(self, files_by_scan_pid):
+ Called when user changes the program's preferences
"""
- 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.
+ 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 postPreferenceChange(self):
"""
- 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
+ Handle changes in program preferences after the preferences dialog window has been closed
+ """
+ if self.rerunSetupAvailableImageAndVideoMedia:
+ if self.usingVolumeMonitor():
+ self.startVolumeMonitor()
+ cmd_line("\n" + _("Download device settings preferences were changed."))
- # 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.selection_vbox.selection_treeview.clear_all()
+ self.setupAvailableImageAndBackupMedia(onStartup = False, onPreferenceChange = True, doNotAllowAutoStart = True)
+ if is_beta and verbose and False:
+ workers.printWorkerStatus()
- 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.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
- return (valid, invalid_dirs)
+ 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
- def same_file_system(self, file1, file2):
- """Returns True if the files / diretories are on the same file system
+ def regenerateScannedDevices(self, thread_id):
"""
- 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
+ Regenerate the filenames / subfolders / download folders for this thread
-
- def same_file(self, file1, file2):
- """Returns True if the files / directories are the same
+ The user must have adjusted their preferences as the device was being scanned
"""
- f1 = gio.File(file1)
- f2 = gio.File(file2)
-
- file_attributes = "id::file"
- f1_info = f1.query_filesystem_info(file_attributes)
- f1_id = f1_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILE)
- f2_info = f2.query_filesystem_info(file_attributes)
- f2_id = f2_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILE)
- return f1_id == f2_id
- def is_valid_download_dir(self, path, is_photo_dir, show_error_in_log=False):
- """
- Checks the following conditions:
- Does the directory exist?
- Is it writable?
+ if 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
+
+
- 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")
+
+ def on_error_eventbox_button_press_event(self, widget, event):
+ self.prefs.show_log_dialog = True
+ log_dialog.widget.show()
+
+class VMonitor:
+ """ Transistion to gvfs from gnomevfs"""
+ def __init__(self, app):
+ self.app = app
+ if using_gio:
+ self.vmonitor = gio.volume_monitor_get()
+ self.vmonitor.connect("mount-added", self.app.on_volume_mounted)
+ self.vmonitor.connect("mount-removed", self.app.on_volume_unmounted)
else:
- download_folder_type = _("Video")
+ self.vmonitor = gnomevfs.VolumeMonitor()
+ self.vmonitor.connect("volume-mounted", self.app.on_volume_mounted)
+ self.vmonitor.connect("volume-unmounted", self.app.on_volume_unmounted)
- try:
- d = gio.File(path)
- if not d.query_exists(cancellable=None):
- logger.error("%s download folder does not exist: %s",
- download_folder_type, path)
- if show_error_in_log:
- severity = config.WARNING
- problem = _("%(file_type)s download folder does not exist") % {
- 'file_type': download_folder_type}
- details = _("Folder: %s") % path
- self.log_error(severity, problem, details)
- else:
- file_attributes = "standard::type,access::can-read,access::can-write"
- file_info = d.query_filesystem_info(file_attributes)
- file_type = file_info.get_file_type()
-
- if file_type != gio.FILE_TYPE_DIRECTORY and file_type != gio.FILE_TYPE_UNKNOWN:
- logger.error("%s download folder is invalid: %s",
- download_folder_type, path)
- if show_error_in_log:
- severity = config.WARNING
- problem = _("%(file_type)s download folder is invalid") % {
- 'file_type': download_folder_type}
- details = _("Folder: %s") % path
- self.log_error(severity, problem, details)
- else:
- # is the directory writable?
- try:
- temp_dir = tempfile.mkdtemp(prefix="rpd-tmp", dir=path)
- valid = True
- except:
- logger.error("%s is not writable", path)
- if show_error_in_log:
- severity = config.WARNING
- problem = _("%(file_type)s download folder is not writable") % {
- 'file_type': download_folder_type}
- details = _("Folder: %s") % path
- self.log_error(severity, problem, details)
- else:
- f = gio.File(temp_dir)
- f.delete(cancellable=None)
- except gio.Error, inst:
- logger.error("Error checking download directory %s", path)
- logger.error(inst)
+ def get_mounts(self):
+ if using_gio:
+ return self.vmonitor.get_mounts()
+ else:
+ return self.vmonitor.get_mounted_volumes()
- return valid
-
-
-
- # # #
- # Process results and management
- # # #
-
+class Volume:
+ """ Transistion to gvfs from gnomevfs"""
+ def __init__(self, volume):
+ self.volume = volume
+
+ def get_name(self, limit=config.MAX_LENGTH_DEVICE_NAME):
+ if using_gio:
+ v = self.volume.get_name()
+ else:
+ v = self.volume.get_display_name()
+
+ if limit:
+ if len(v) > limit:
+ v = v[:limit] + '...'
+ return v
- def _start_process_managers(self):
- """
- Set up process managers.
+ 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
- A task such as scanning a device or copying files is handled in its
- own process.
- """
- self.batch_size = 10
- self.batch_size_MB = 2
+ def get_icon_pixbuf(self, size):
+ """ returns icon for the volume, or None if not available"""
- 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)
+ 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.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)
+ 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)
+
- def scan_results(self, source, condition):
- """
- Receive results from scan processes
- """
- connection = self.scan_manager.get_pipe(source)
+ 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.
- conn_type, data = connection.recv()
+ 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 conn_type == rpdmp.CONN_COMPLETE:
- connection.close()
- 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.testing_auto_exit_trip_counter += 1
- if self.testing_auto_exit_trip_counter == self.testing_auto_exit_trip and self.testing_auto_exit:
- self.on_rapidapp_destroy(self.rapidapp)
- else:
- if not self.testing_auto_exit:
- self.download_progressbar.set_text(_("Thumbnails"))
- self.thumbnails.generate_thumbnails(scan_pid)
- self.set_download_action_sensitivity()
- self.set_thumbnail_sort()
-
- # 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))
+ 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:
- for rpd_file in data:
- self.thumbnails.add_file(rpd_file)
+ return (-99, None)
+ return (1, None)
- # must return True for this method to be called again
- return True
+ 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])
+class TimeForDownload:
+ # used to store variables, see below
+ pass
+
+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
+
+ def timeRemaining(self):
+ return max(self._timeEstimates())
- @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 setTimeMark(self, w):
+ if w in self.times:
+ self.times[w].timeMark = time.time()
+
+ def clear(self):
+ self.times = {}
+
+ def remove(self, w):
+ if w in self.times:
+ del self.times[w]
-def start():
+def programStatus():
+ print _("Goodbye")
- is_beta = config.version.find('~') > 0
+
+def start ():
+ global is_beta
+ is_beta = config.version.find('~b') > 0
- parser = OptionParser(version= "%%prog %s" % utilities.human_readable_version(config.version))
+ parser = OptionParser(version= "%%prog %s" % config.version)
parser.set_defaults(verbose=is_beta, extensions=False)
# Translators: this text is displayed to the user when they request information on the command line options.
# The text %default should not be modified or left out.
@@ -2637,18 +6468,19 @@ 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
- if options.debug:
- logging_level = logging.DEBUG
- elif options.verbose:
- logging_level = logging.INFO
- else:
- logging_level = logging.ERROR
+ global debug_info
+ debug_info = options.debug
+ if debug_info:
+ verbose = True
- logger.setLevel(logging_level)
-
+ if verbose:
+ atexit.register(programStatus)
+
if options.extensions:
- extensions = ((rpdfile.RAW_FILE_EXTENSIONS + rpdfile.NON_RAW_IMAGE_FILE_EXTENSIONS, _("Photos:")), (rpdfile.VIDEO_FILE_EXTENSIONS, _("Videos:")))
+ extensions = ((metadata.RAW_FILE_EXTENSIONS + metadata.NON_RAW_IMAGE_FILE_EXTENSIONS, _("Photos:")), (videometadata.VIDEO_FILE_EXTENSIONS, _("Videos:")))
for exts, file_type in extensions:
v = ''
for e in exts[:-1]:
@@ -2664,25 +6496,43 @@ def start():
print _("All settings and preferences have been reset")
sys.exit(0)
- 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())
+ cmd_line(_("Rapid Photo Downloader") + " %s" % config.version)
+ cmd_line(_("Using") + " pyexiv2 " + metadata.version_info())
+ cmd_line(_("Using") + " exiv2 " + metadata.exiv2_version_info())
if DOWNLOAD_VIDEO:
- logger.info("Using hachoir %s", metadatavideo.version_info())
+ cmd_line(_("Using") + " hachoir " + videometadata.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:
- logger.info(_("Video downloading functionality disabled.\nTo download videos, please install the hachoir metadata and kaa metadata packages for python."))
+ # Which volume management code is being used (GIO or GnomeVFS)
+ cmd_line(_("Using") + " GnomeVFS")
+ gdk.threads_init()
+
+
+
+ display_queue.open("rw")
+ tube.tube_add_watch(display_queue, updateDisplay)
+ gdk.threads_enter()
+
+ # run only a single instance of the application
bus = dbus.SessionBus ()
request = bus.request_name (config.DBUS_NAME, dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
- if request != dbus.bus.REQUEST_NAME_REPLY_EXISTS:
- app = RapidApp(bus, '/', config.DBUS_NAME)
+ if request != dbus.bus.REQUEST_NAME_REPLY_EXISTS:
+ app = RapidApp (bus, '/', config.DBUS_NAME)
else:
# this application is already running
- print "Rapid Photo Downloader is already running"
+ print _("%s is already running") % PROGRAM_NAME
object = bus.get_object (config.DBUS_NAME, "/")
app = dbus.Interface (object, config.DBUS_NAME)
- app.start()
+ app.start()
+
+ gdk.threads_leave()
if __name__ == "__main__":
start()