summaryrefslogtreecommitdiff
path: root/rapid/rapid.py
diff options
context:
space:
mode:
Diffstat (limited to 'rapid/rapid.py')
-rwxr-xr-xrapid/rapid.py670
1 files changed, 521 insertions, 149 deletions
diff --git a/rapid/rapid.py b/rapid/rapid.py
index 3603025..df60790 100755
--- a/rapid/rapid.py
+++ b/rapid/rapid.py
@@ -36,6 +36,7 @@ import webbrowser
import sys, time, types, os, datetime
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
@@ -127,9 +128,12 @@ class DeviceCollection(gtk.TreeView):
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)
+ # copy progress (%), copy text, eject button (None if irrelevant),
+ # process id
+ self.liststore = gtk.ListStore(gtk.gdk.Pixbuf, str, str, float, str,
+ gtk.gdk.Pixbuf, int)
self.map_process_to_row = {}
+ self.devices_by_scan_pid = {}
gtk.TreeView.__init__(self, self.liststore)
@@ -145,11 +149,14 @@ class DeviceCollection(gtk.TreeView):
pixbuf_renderer = gtk.CellRendererPixbuf()
text_renderer = gtk.CellRendererText()
text_renderer.props.ellipsize = pango.ELLIPSIZE_MIDDLE
- text_renderer.set_fixed_size(160, -1)
+ text_renderer.set_fixed_size(160, -1)
+ eject_renderer = gtk.CellRendererPixbuf()
column0.pack_start(pixbuf_renderer, expand=False)
column0.pack_start(text_renderer, expand=True)
+ column0.pack_end(eject_renderer, expand=False)
column0.add_attribute(pixbuf_renderer, 'pixbuf', 0)
column0.add_attribute(text_renderer, 'text', 1)
+ column0.add_attribute(eject_renderer, 'pixbuf', 5)
self.append_column(column0)
@@ -165,19 +172,49 @@ class DeviceCollection(gtk.TreeView):
self.append_column(column2)
self.show_all()
+ icontheme = gtk.icon_theme_get_default()
+ try:
+ self.eject_pixbuf = icontheme.load_icon('media-eject', 16,
+ gtk.ICON_LOOKUP_USE_BUILTIN)
+ except:
+ self.eject_pixbuf = gtk.gdk.pixbuf_new_from_file(
+ paths.share_dir('glade3/media-eject.png'))
+
+ self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
+ self.connect('button-press-event', self.button_clicked)
+
+
def add_device(self, process_id, device, progress_bar_text = ''):
# add the row, and get a temporary pointer to the row
size_files = ''
progress = 0.0
+
+ if device.mount is None:
+ eject = None
+ else:
+ eject = self.eject_pixbuf
+
+ self.devices_by_scan_pid[process_id] = device
+
iter = self.liststore.append((device.get_icon(),
device.get_name(),
size_files,
progress,
- progress_bar_text))
+ progress_bar_text,
+ eject,
+ process_id))
self._set_process_map(process_id, iter)
+
+ # adjust scrolled window height, based on row height and number of ready to start downloads
+ # please note, at program startup, self.row_height() will be less than it will be when already running
+ # e.g. when starting with 3 cards, it could be 18, but when adding 2 cards to the already running program
+ # (with one card at startup), it could be 21
+ row_height = self.get_background_area(0, self.get_column(0))[3] + 1
+ height = (len(self.map_process_to_row) + 1) * row_height
+ self.parent_app.device_collection_scrolledwindow.set_size_request(-1, height)
def update_device(self, process_id, total_size_files):
"""
@@ -187,13 +224,17 @@ class DeviceCollection(gtk.TreeView):
iter = self._get_process_map(process_id)
self.liststore.set_value(iter, 2, total_size_files)
else:
- logger.error("This device is unknown")
+ logger.critical("This device is unknown")
+
+ def get_device(self, process_id):
+ return self.devices_by_scan_pid.get(process_id)
def remove_device(self, process_id):
if process_id in self.map_process_to_row:
iter = self._get_process_map(process_id)
self.liststore.remove(iter)
del self.map_process_to_row[process_id]
+ del self.devices_by_scan_pid[process_id]
def get_all_displayed_processes(self):
"""
@@ -237,6 +278,44 @@ class DeviceCollection(gtk.TreeView):
pass
#~ logger.info("Implement update overall progress")
+ def button_clicked(self, widget, event):
+ """
+ Look for left single click on eject button
+ """
+ if event.button == 1:
+ x = int(event.x)
+ y = int(event.y)
+ path, column, cell_x, cell_y = self.get_path_at_pos(x, y)
+ if path is not None:
+ if column == self.get_column(0):
+ if cell_x >= column.get_width() - self.eject_pixbuf.get_width():
+ iter = self.liststore.get_iter(path)
+ if self.liststore.get_value(iter, 5) is not None:
+ self.unmount(process_id = self.liststore.get_value(iter, 6))
+
+ def unmount(self, process_id):
+ device = self.devices_by_scan_pid[process_id]
+ if device.mount is not None:
+ logger.debug("Unmounting device with scan pid %s", process_id)
+ device.mount.unmount(self.unmount_callback)
+
+
+ def unmount_callback(self, mount, result):
+ name = mount.get_name()
+
+ try:
+ mount.unmount_finish(result)
+ logger.debug("%s successfully unmounted" % name)
+ except gio.Error, inst:
+ logger.error("%s did not unmount: %s", name, inst)
+
+ title = _("%(device)s did not unmount") % {'device': name}
+ message = '%s' % inst
+
+ n = pynotify.Notification(title, message)
+ n.set_icon_from_pixbuf(self.parent_app.application_icon)
+ n.show()
+
def create_cairo_image_surface(pil_image, image_width, image_height):
imgd = pil_image.tostring("raw","BGRA", 0, 1)
@@ -283,7 +362,6 @@ class ThumbnailCellRenderer(gtk.CellRenderer):
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)
@@ -316,9 +394,8 @@ class ThumbnailCellRenderer(gtk.CellRenderer):
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.set_source_rgb(0.33,0.33,0.33)
+ #~ cairo_context.rectangle(x, y, w, h)
#~ cairo_context.stroke()
#place the image
@@ -362,7 +439,6 @@ class ThumbnailCellRenderer(gtk.CellRenderer):
cairo_context.paint()
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)
@@ -372,6 +448,10 @@ gobject.type_register(ThumbnailCellRenderer)
class ThumbnailDisplay(gtk.IconView):
def __init__(self, parent_app):
gtk.IconView.__init__(self)
+ self.set_spacing(0)
+ self.set_row_spacing(5)
+ self.set_margin(25)
+
self.rapid_app = parent_app
self.batch_size = 10
@@ -384,7 +464,7 @@ class ThumbnailDisplay(gtk.IconView):
self.rpd_files = {}
- self.total_files = 0
+ self.total_thumbs_to_generate = 0
self.thumbnails_generated = 0
self.thumbnails = {}
@@ -414,16 +494,17 @@ class ThumbnailDisplay(gtk.IconView):
gtk.gdk.Pixbuf, # 8 status icon
)
-
self.clear()
self.set_model(self.liststore)
+
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)
@@ -431,29 +512,22 @@ class ThumbnailDisplay(gtk.IconView):
checkbutton_height = checkbutton_size[3]
checkbutton_width = checkbutton_size[2]
- #~ status_icon = gtk.CellRendererPixbuf()
- #~ self.pack_start(status_icon, expand=False)
- #~ self.add_attribute(status_icon, "pixbuf", self.STATUS_ICON_COL)
-
image = ThumbnailCellRenderer(checkbutton_height)
self.pack_start(image, expand=True)
self.add_attribute(image, "image", 0)
self.add_attribute(image, "filename", 3)
self.add_attribute(image, "status", 8)
+
#set the background color to a darkish grey
self.modify_base(gtk.STATE_NORMAL, gtk.gdk.Color('#444444'))
- self.set_spacing(0)
- #~ self.set_column_spacing(0)
- self.set_row_spacing(5)
- #~ self.set_row_spacing(0)
- self.set_margin(25)
+ self.show_all()
self._setup_icons()
-
- self.show_all()
+
+
self.connect('item-activated', self.on_item_activated)
@@ -474,7 +548,7 @@ class ThumbnailDisplay(gtk.IconView):
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'),
+ paths.share_dir('glade3/rapid-photo-downloader-download-pending.png'),
size, size)
self.downloaded_with_warning_icon = gtk.gdk.pixbuf_new_from_file_at_size(
paths.share_dir('glade3/rapid-photo-downloader-downloaded-with-warning.svg'),
@@ -523,7 +597,7 @@ class ThumbnailDisplay(gtk.IconView):
iter = self.get_iter_from_unique_id(unique_id)
self.liststore.set_value(iter, self.SELECTED_COL, value)
- def add_file(self, rpd_file):
+ def add_file(self, rpd_file, generate_thumbnail):
thumbnail_icon = self.get_stock_icon(rpd_file.file_type)
unique_id = rpd_file.unique_id
@@ -553,7 +627,8 @@ class ThumbnailDisplay(gtk.IconView):
self.treerow_index[unique_id] = treerowref
self.rpd_files[unique_id] = rpd_file
- self.total_files += 1
+ if generate_thumbnail:
+ self.total_thumbs_to_generate += 1
def get_sample_file(self, file_type):
"""Returns an rpd_file for of a given file type, or None if it does
@@ -701,24 +776,56 @@ class ThumbnailDisplay(gtk.IconView):
return True
return False
- def get_files_checked_for_download(self):
+ def get_files_checked_for_download(self, scan_pid):
"""
Returns a dict of scan ids and associated files the user has indicated
they want to download
+
+ If scan_pid is not None, then returns only those files from that scan_pid
"""
files = dict()
- for row in self.liststore:
- if row[self.SELECTED_COL]:
- rpd_file = self.rpd_files[row[self.UNIQUE_ID_COL]]
+ if scan_pid is None:
+ for row in self.liststore:
+ if row[self.SELECTED_COL]:
+ rpd_file = self.rpd_files[row[self.UNIQUE_ID_COL]]
+ if rpd_file.status not in DOWNLOADED:
+ scan_pid = rpd_file.scan_pid
+ if scan_pid in files:
+ files[scan_pid].append(rpd_file)
+ else:
+ files[scan_pid] = [rpd_file,]
+ else:
+ files[scan_pid] = []
+ for unique_id in self.process_index[scan_pid]:
+ rpd_file = self.rpd_files[unique_id]
if rpd_file.status not in DOWNLOADED:
- scan_pid = rpd_file.scan_pid
- if scan_pid in files:
+ iter = self.get_iter_from_unique_id(unique_id)
+ if self.liststore.get_value(iter, self.SELECTED_COL):
files[scan_pid].append(rpd_file)
- else:
- files[scan_pid] = [rpd_file,]
-
return files
+ def get_no_files_remaining(self, scan_pid):
+ """
+ Returns the number of files that have not yet been downloaded for the
+ scan_pid
+ """
+ i = 0
+ for unique_id in self.process_index[scan_pid]:
+ rpd_file = self.rpd_files[unique_id]
+ if rpd_file.status == STATUS_NOT_DOWNLOADED:
+ i += 1
+ return i
+
+ def files_remain_to_download(self):
+ """
+ Returns True if any files remain that are not downloaded, else returns
+ False
+ """
+ for row in self.liststore:
+ if row[self.DOWNLOAD_STATUS_COL] == STATUS_NOT_DOWNLOADED:
+ return True
+ return False
+
def mark_download_pending(self, files_by_scan_pid):
"""
@@ -729,7 +836,11 @@ class ThumbnailDisplay(gtk.IconView):
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)
+ if not self.rapid_app.auto_start_is_on:
+ # don't make the checkbox invisible immediately when on auto start
+ # otherwise the box can be rendred at the wrong size, as it is
+ # realized after the checkbox has already been made invisible
+ self.liststore.set_value(iter, self.CHECKBUTTON_VISIBLE_COL, False)
self.liststore.set_value(iter, self.SELECTED_COL, False)
self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, STATUS_DOWNLOAD_PENDING)
icon = self.get_status_icon(STATUS_DOWNLOAD_PENDING)
@@ -752,6 +863,7 @@ class ThumbnailDisplay(gtk.IconView):
self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, rpd_file.status)
icon = self.get_status_icon(rpd_file.status)
self.liststore.set_value(iter, self.STATUS_ICON_COL, icon)
+ self.liststore.set_value(iter, self.CHECKBUTTON_VISIBLE_COL, False)
self.rpd_files[rpd_file.unique_id] = rpd_file
def generate_thumbnails(self, scan_pid):
@@ -803,12 +915,15 @@ class ThumbnailDisplay(gtk.IconView):
# clear progress bar information if all thumbnails have been
# extracted
- if self.thumbnails_generated == self.total_files:
+ if self.thumbnails_generated == self.total_thumbs_to_generate:
self.rapid_app.download_progressbar.set_fraction(0.0)
self.rapid_app.download_progressbar.set_text('')
+ self.thumbnails_generated = 0
+ self.total_thumbs_to_generate = 0
+
else:
self.rapid_app.download_progressbar.set_fraction(
- float(self.thumbnails_generated) / self.total_files)
+ float(self.thumbnails_generated) / self.total_thumbs_to_generate)
return True
@@ -865,11 +980,13 @@ class TaskManager:
self.batch_size = batch_size
self.paused = False
+ self.no_tasks = 0
def add_task(self, task):
pid = self._setup_task(task)
logger.debug("TaskManager PID: %s", pid)
+ self.no_tasks += 1
return pid
@@ -982,10 +1099,12 @@ class CopyFilesManager(TaskManager):
video_download_folder = task[1]
scan_pid = task[2]
files = task[3]
+ generate_thumbnails = task[4]
copy_files = copyfiles.CopyFiles(photo_download_folder,
video_download_folder,
- files, scan_pid, self.batch_size,
+ files, generate_thumbnails,
+ scan_pid, self.batch_size,
task_process_conn, terminate_queue, run_event)
copy_files.start()
self._processes.append((copy_files, terminate_queue, run_event))
@@ -1188,6 +1307,7 @@ class RapidApp(dbus.service.Object):
# Initialize widgets in the main window, and variables that point to them
self._init_widgets()
+ self._init_pynotify()
# Initialize job code handling
self._init_job_code()
@@ -1205,8 +1325,9 @@ class RapidApp(dbus.service.Object):
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
+ prefs_valid, msg = prefsrapid.check_prefs_for_validity(self.prefs)
+ if not prefs_valid:
+ self.notify_prefs_are_invalid(details=msg)
# Initialize variables with which to track important downloads results
self._init_download_tracking()
@@ -1218,12 +1339,10 @@ class RapidApp(dbus.service.Object):
# 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)
+ block_auto_start=not prefs_valid)
# 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):
@@ -1312,7 +1431,7 @@ class RapidApp(dbus.service.Object):
def on_refresh_action_activate(self, action):
self.setup_devices(on_startup=False, on_preference_change=False,
- do_not_allow_auto_start=True)
+ block_auto_start=True)
def on_get_help_action_activate(self, action):
webbrowser.open("http://www.damonlynch.net/rapid/help.html")
@@ -1379,7 +1498,7 @@ class RapidApp(dbus.service.Object):
self.vmonitor.connect("mount-removed", self.on_mount_removed)
- def setup_devices(self, on_startup, on_preference_change, do_not_allow_auto_start):
+ def setup_devices(self, on_startup, on_preference_change, block_auto_start):
"""
Setup devices from which to download from and backup to
@@ -1392,6 +1511,9 @@ class RapidApp(dbus.service.Object):
on_preference_change should be True if this is being called as the
result of a preference being changed
+ block_auto_start should be True if automation options to automatically
+ start a download should be ignored
+
Removes any image media that are currently not downloaded,
or finished downloading
"""
@@ -1449,7 +1571,7 @@ class RapidApp(dbus.service.Object):
# Display amount of free space in a status bar message
self.display_free_space()
- if do_not_allow_auto_start:
+ if block_auto_start:
self.auto_start_is_on = False
else:
self.auto_start_is_on = ((not on_preference_change) and
@@ -1475,6 +1597,8 @@ class RapidApp(dbus.service.Object):
scan_pid = self.scan_manager.add_task(device)
if mount is not None:
self.mounts_by_path[path] = scan_pid
+ if not mounts:
+ self.set_download_action_sensitivity()
def get_use_device(self, device):
""" Prompt user whether or not to download from this device """
@@ -1553,28 +1677,30 @@ class RapidApp(dbus.service.Object):
return
path = mount.get_root().get_path()
+ if path is not None:
- if path in self.prefs.device_blacklist and self.search_for_PSD():
- logger.info("Device %(device)s (%(path)s) ignored" % {
- 'device': mount.get_name(), 'path': path})
- else:
- is_backup_mount = 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()
-
- 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
+ if path in self.prefs.device_blacklist and self.search_for_PSD():
+ logger.info("Device %(device)s (%(path)s) ignored" % {
+ 'device': mount.get_name(), 'path': path})
+ else:
+ is_backup_mount = 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()
+
+ elif self.prefs.device_autodetection and (dv.is_DCIM_device(path) or
+ self.search_for_PSD()):
+
+ self.auto_start_is_on = self.prefs.auto_download_upon_device_insertion
+ device = dv.Device(path=path, mount=mount)
+ if self.search_for_PSD() and path not in self.prefs.device_whitelist:
+ # prompt user if device should be used or not
+ self.get_use_device(device)
+ else:
+ scan_pid = self.scan_manager.add_task(device)
+ self.mounts_by_path[path] = scan_pid
def on_mount_removed(self, vmonitor, mount):
"""
@@ -1643,7 +1769,10 @@ class RapidApp(dbus.service.Object):
logger.debug("Download activated")
if self.download_action_is_download:
- self.start_download()
+ if self.need_job_code_for_naming and not self.prompting_for_job_code:
+ self.get_job_code()
+ else:
+ self.start_download()
else:
self.pause_download()
@@ -1663,10 +1792,10 @@ class RapidApp(dbus.service.Object):
"""
if not self.download_is_occurring():
sensitivity = False
- if self.scan_manager.get_no_active_processes() == 0:
+ if self.scan_manager.no_tasks == 0:
if self.thumbnails.files_are_checked_to_download():
sensitivity = True
-
+
self.download_action.set_sensitive(sensitivity)
def set_download_action_label(self, is_download):
@@ -1687,21 +1816,19 @@ class RapidApp(dbus.service.Object):
def _init_job_code(self):
- self.job_code = None
+ self.job_code = self.last_chosen_job_code = ''
self.need_job_code_for_naming = self.prefs.any_pref_uses_job_code()
-
- def assign_job_code(self, code):
- """ assign job code (which may be empty) to global variable and update user preferences
+ self.prompting_for_job_code = False
+
+ def assign_job_code(self, code):
+ """ assign job code (which may be empty) to member variable and update user preferences
Update preferences only if code is not empty. Do not duplicate job code.
"""
- # FIXME
- #~ global job_code
- if code == None:
- code = ''
- job_code = code
+
+ self.job_code = code
- if job_code:
+ if code:
#add this value to job codes preferences
#delete any existing value which is the same
#(this way it comes to the front, which is where it should be)
@@ -1713,7 +1840,7 @@ class RapidApp(dbus.service.Object):
self.prefs.job_codes = [code] + jcs
- def _get_job_code(self, post_job_code_entry_callback, autoStart, downloadSelected):
+ def _get_job_code(self, post_job_code_entry_callback):
""" prompt for a job code """
if not self.prompting_for_job_code:
@@ -1735,29 +1862,19 @@ class RapidApp(dbus.service.Object):
self.prompting_for_job_code = False
if user_chose_code:
+ if code is None:
+ code = ''
self.assign_job_code(code)
self.last_chosen_job_code = code
- #~ 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()
+ logger.debug("Job Code %s entered", self.job_code)
+ self.start_download()
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
+ logger.debug("No Job Code entered")
+ self.job_code = ''
+ self.auto_start_is_on = False
+
# # #
# Download
@@ -1780,13 +1897,14 @@ class RapidApp(dbus.service.Object):
- def start_download(self):
+ def start_download(self, scan_pid=None):
"""
Start download, renaming and backup of files.
+
+ If scan_pid is specified, only files matching it will be downloaded
"""
- self.download_start_time = datetime.datetime.now()
- files_by_scan_pid = self.thumbnails.get_files_checked_for_download()
+ files_by_scan_pid = self.thumbnails.get_files_checked_for_download(scan_pid)
folders_valid, invalid_dirs = self.check_download_folder_validity(files_by_scan_pid)
if not folders_valid:
@@ -1798,6 +1916,11 @@ class RapidApp(dbus.service.Object):
self.log_error(config.CRITICAL_ERROR, _("Download cannot proceed"),
msg)
else:
+ # set time download is starting if it is not already set
+ # it is unset when all downloads are completed
+ if self.download_start_time is None:
+ self.download_start_time = datetime.datetime.now()
+
self.thumbnails.mark_download_pending(files_by_scan_pid)
for scan_pid in files_by_scan_pid:
files = files_by_scan_pid[scan_pid]
@@ -1813,7 +1936,14 @@ class RapidApp(dbus.service.Object):
if not self.download_action_is_download:
self.set_download_action_label(is_download = True)
+ self.time_check.pause()
+
def resume_download(self):
+ for scan_pid in self.download_active_by_scan_pid:
+ self.time_remaining.set_time_mark(scan_pid)
+
+ self.time_check.set_download_mark()
+
self.copy_files_manager.start()
def download_files(self, files, scan_pid):
@@ -1831,16 +1961,25 @@ class RapidApp(dbus.service.Object):
video_download_folder = self.prefs.video_download_folder
else:
video_download_folder = None
-
+
+ download_size = self.size_files_to_be_downloaded(files)
self.download_tracker.init_stats(scan_pid=scan_pid,
- bytes=self.size_files_to_be_downloaded(files),
+ bytes=download_size,
no_files=len(files))
+
+ self.time_remaining.set(scan_pid, download_size)
+ self.time_check.set_download_mark()
self.download_active_by_scan_pid.append(scan_pid)
+
+
+ if len(self.download_active_by_scan_pid) > 1:
+ self.display_summary_notification = True
+
# Initiate copy files process
self.copy_files_manager.add_task((photo_download_folder,
video_download_folder, scan_pid,
- files))
+ files, self.auto_start_is_on))
def copy_files_results(self, source, condition):
"""
@@ -1856,12 +1995,14 @@ class RapidApp(dbus.service.Object):
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
+ scan_pid, total_downloaded, chunk_downloaded = data
self.download_tracker.set_total_bytes_copied(scan_pid,
total_downloaded)
+ self.time_check.increment(bytes_downloaded=chunk_downloaded)
percent_complete = self.download_tracker.get_percent_complete(scan_pid)
self.device_collection.update_progress(scan_pid, percent_complete,
None, None)
+ self.time_remaining.update(scan_pid, total_downloaded)
elif msg_type == rpdmp.MSG_FILE:
download_succeeded, rpd_file, download_count, temp_full_file_name = data
@@ -1877,14 +2018,18 @@ class RapidApp(dbus.service.Object):
rpd_file.strip_characters = self.prefs.strip_characters
rpd_file.download_folder = self.prefs.get_download_folder_for_file_type(rpd_file.file_type)
rpd_file.download_conflict_resolution = self.prefs.download_conflict_resolution
- rpd_file.synchronize_raw_jpg = self.prefs.must_synchronize_raw_jpg()
+ rpd_file.synchronize_raw_jpg = self.prefs.must_synchronize_raw_jpg()
+ rpd_file.job_code = self.job_code
self.subfolder_file_manager.rename_file_and_move_to_subfolder(
download_succeeded,
download_count,
rpd_file
)
-
+ elif msg_type == rpdmp.MSG_THUMB:
+ #~ unique_id, thumbnail, thumbnail_icon = data
+ #~ thumbnail_data = (unique_id
+ self.thumbnails.update_thumbnail(data)
return True
else:
@@ -1897,7 +2042,9 @@ class RapidApp(dbus.service.Object):
def download_is_occurring(self):
"""Returns True if a file is currently being downloaded or renamed
"""
- return not len(self.download_active_by_scan_pid) == 0
+ v = not len(self.download_active_by_scan_pid) == 0
+ #~ logger.info("Download is occurring: %s", v)
+ return v
# # #
# Create folder and file names for downloaded files
@@ -1921,48 +2068,233 @@ class RapidApp(dbus.service.Object):
self.log_error(config.WARNING, rpd_file.error_title,
rpd_file.error_msg, rpd_file.error_extra_detail)
- self.download_tracker.file_downloaded_increment(scan_pid)
- self._update_file_download_device_progress(scan_pid, unique_id)
+ self.download_tracker.file_downloaded_increment(scan_pid,
+ rpd_file.file_type,
+ rpd_file.status)
+
+ completed, files_remaining = self._update_file_download_device_progress(scan_pid, unique_id)
+
+ if self.download_is_occurring():
+ self.update_time_remaining()
- 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
+ if completed:
+ # Last file for this scan pid has been downloaded, so clean temp directory
logger.debug("Purging temp directories")
self._clean_temp_dirs_for_scan_pid(scan_pid)
- self.download_tracker.purge(scan_pid)
self.download_active_by_scan_pid.remove(scan_pid)
+ self.time_remaining.remove(scan_pid)
+ self.notify_downloaded_from_device(scan_pid)
+ if files_remaining == 0 and self.prefs.auto_unmount:
+ self.device_collection.unmount(scan_pid)
+
if not self.download_is_occurring():
logger.debug("Download completed")
+ self.notify_download_complete()
+ self.download_progressbar.set_fraction(0.0)
self.prefs.stored_sequence_no = self.stored_sequence_value.value
self.downloads_today_tracker.set_raw_downloads_today_from_int(self.downloads_today_value.value)
self.downloads_today_tracker.set_raw_downloads_today_date(self.downloads_today_date_value.value)
self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker)
-
+
+ if ((self.prefs.auto_exit and self.download_tracker.no_errors_or_warnings())
+ or self.prefs.auto_exit_force):
+ if not self.thumbnails.files_remain_to_download():
+ gtk.main_quit()
+
+ self.download_tracker.purge_all()
+ self.speed_label.set_label(" ")
+
self.display_free_space()
self.set_download_action_label(is_download=True)
self.set_download_action_sensitivity()
+
+ self.job_code = ''
+ self.download_start_time = None
+
+
+ def update_time_remaining(self):
+ update, download_speed = self.time_check.check_for_update()
+ if update:
+ self.speed_label.set_text(download_speed)
+
+ time_remaining = self.time_remaining.time_remaining()
+ if time_remaining:
+ secs = int(time_remaining)
+
+ if secs == 0:
+ message = ""
+ elif secs == 1:
+ message = _("About 1 second remaining")
+ elif secs < 60:
+ message = _("About %i seconds remaining") % secs
+ elif secs == 60:
+ message = _("About 1 minute remaining")
+ else:
+ # Translators: in the text '%(minutes)i:%(seconds)02i', only the : should be translated, if needed.
+ # '%(minutes)i' and '%(seconds)02i' should not be modified or left out. They are used to format and display the amount
+ # of time the download has remainging, e.g. 'About 5:36 minutes remaining'
+ message = _("About %(minutes)i:%(seconds)02i minutes remaining") % {'minutes': secs / 60, 'seconds': secs % 60}
+
+ self.rapid_statusbar.pop(self.statusbar_context_id)
+ self.rapid_statusbar.push(self.statusbar_context_id, message)
+
+ def file_types_by_number(self, no_photos, no_videos):
+ """
+ returns a string to be displayed to the user that can be used
+ to show if a value refers to photos or videos or both, or just one
+ of each
+ """
+ if (no_videos > 0) and (no_photos > 0):
+ v = _('photos and videos')
+ elif (no_videos == 0) and (no_photos == 0):
+ v = _('photos or videos')
+ elif no_videos > 0:
+ if no_videos > 1:
+ v = _('videos')
+ else:
+ v = _('video')
else:
- pass
- #~ logger.info("Download count: %s", download_count)
-
+ if no_photos > 1:
+ v = _('photos')
+ else:
+ v = _('photo')
+ return v
+ def notify_downloaded_from_device(self, scan_pid):
+ device = self.device_collection.get_device(scan_pid)
+
+ if device.mount is None:
+ notificationName = PROGRAM_NAME
+ else:
+ notificationName = device.get_name()
+
+ no_photos_downloaded = self.download_tracker.get_no_files_downloaded(
+ scan_pid, rpdfile.FILE_TYPE_PHOTO)
+ no_videos_downloaded = self.download_tracker.get_no_files_downloaded(
+ scan_pid, rpdfile.FILE_TYPE_VIDEO)
+ no_photos_failed = self.download_tracker.get_no_files_failed(
+ scan_pid, rpdfile.FILE_TYPE_PHOTO)
+ no_videos_failed = self.download_tracker.get_no_files_failed(
+ scan_pid, rpdfile.FILE_TYPE_VIDEO)
+ no_files_downloaded = no_photos_downloaded + no_videos_downloaded
+ no_files_failed = no_photos_failed + no_videos_failed
+ no_warnings = self.download_tracker.get_no_warnings(scan_pid)
+
+ file_types = self.file_types_by_number(no_photos_downloaded, no_videos_downloaded)
+ file_types_failed = self.file_types_by_number(no_photos_failed, no_videos_failed)
+ message = _("%(noFiles)s %(filetypes)s downloaded") % \
+ {'noFiles':no_files_downloaded, 'filetypes': file_types}
+
+ if no_files_failed:
+ message += "\n" + _("%(noFiles)s %(filetypes)s failed to download") % {'noFiles':no_files_failed, 'filetypes':file_types_failed}
+
+ if no_warnings:
+ message = "%s\n%s " % (message, no_warnings) + _("warnings")
+
+ n = pynotify.Notification(notificationName, message)
+ n.set_icon_from_pixbuf(device.get_icon(self.notification_icon_size))
+
+ n.show()
+
+ def notify_download_complete(self):
+ if self.display_summary_notification:
+ message = _("All downloads complete")
+
+ # photo downloads
+ photo_downloads = self.download_tracker.total_photos_downloaded
+ if photo_downloads:
+ filetype = self.file_types_by_number(photo_downloads, 0)
+ message += "\n" + _("%(number)s %(numberdownloaded)s") % \
+ {'number': photo_downloads,
+ 'numberdownloaded': _("%(filetype)s downloaded") % \
+ {'filetype': filetype}}
+
+ # photo failures
+ photo_failures = self.download_tracker.total_photo_failures
+ if photo_failures:
+ filetype = self.file_types_by_number(photo_failures, 0)
+ message += "\n" + _("%(number)s %(numberdownloaded)s") % \
+ {'number': photo_failures,
+ 'numberdownloaded': _("%(filetype)s failed to download") % \
+ {'filetype': filetype}}
+
+ # video downloads
+ video_downloads = self.download_tracker.total_videos_downloaded
+ if video_downloads:
+ filetype = self.file_types_by_number(0, video_downloads)
+ message += "\n" + _("%(number)s %(numberdownloaded)s") % \
+ {'number': video_downloads,
+ 'numberdownloaded': _("%(filetype)s downloaded") % \
+ {'filetype': filetype}}
+
+ # video failures
+ video_failures = self.download_tracker.total_video_failures
+ if video_failures:
+ filetype = self.file_types_by_number(0, video_failures)
+ message += "\n" + _("%(number)s %(numberdownloaded)s") % \
+ {'number': video_failures,
+ 'numberdownloaded': _("%(filetype)s failed to download") % \
+ {'filetype': filetype}}
+
+ # warnings
+ warnings = self.download_tracker.total_warnings
+ if warnings:
+ message += "\n" + _("%(number)s %(numberdownloaded)s") % \
+ {'number': warnings,
+ 'numberdownloaded': _("warnings")}
+
+ n = pynotify.Notification(PROGRAM_NAME, message)
+ n.set_icon_from_pixbuf(self.application_icon)
+ n.show()
+ self.display_summary_notification = False # don't show it again unless needed
+
def _update_file_download_device_progress(self, scan_pid, unique_id):
"""
Increments the progress bar for an individual device
+
+ Returns if the download is completed for that scan_pid
+ It also returns the number of files remaining for the scan_pid, BUT
+ this value is valid ONLY if the download is completed
"""
- 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)}
+
+ files_downloaded = self.download_tracker.get_download_count_for_file(unique_id)
+ files_to_download = self.download_tracker.get_no_files_in_download(scan_pid)
+ file_types = self.download_tracker.get_file_types_present(scan_pid)
+ completed = files_downloaded == files_to_download
+
+ if completed:
+ files_remaining = self.thumbnails.get_no_files_remaining(scan_pid)
+ else:
+ files_remaining = 0
+
+ if completed and files_remaining:
+ # e.g.: 3 of 205 photos and videos (202 remaining)
+ progress_bar_text = _("%(number)s of %(total)s %(filetypes)s (%(remaining)s remaining)") % {
+ 'number': files_downloaded,
+ 'total': files_to_download,
+ 'filetypes': file_types,
+ 'remaining': files_remaining}
+ else:
+ # e.g.: 205 of 205 photos and videos
+ progress_bar_text = _("%(number)s of %(total)s %(filetypes)s") % \
+ {'number': files_downloaded,
+ 'total': files_to_download,
+ 'filetypes': file_types}
percent_complete = self.download_tracker.get_percent_complete(scan_pid)
self.device_collection.update_progress(scan_pid=scan_pid,
percent_complete=percent_complete,
progress_bar_text=progress_bar_text,
- bytes_downloaded=None)
+ bytes_downloaded=None)
+
+ percent_complete = self.download_tracker.get_overall_percent_complete()
+ self.download_progressbar.set_fraction(percent_complete)
+
+ return (completed, files_remaining)
+
def _clean_all_temp_dirs(self):
"""
@@ -1999,7 +2331,6 @@ class RapidApp(dbus.service.Object):
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)
@@ -2011,20 +2342,6 @@ class RapidApp(dbus.service.Object):
# # #
# Preferences
# # #
-
- def check_prefs_on_startup(self):
- """
- Checks the image & video rename, and subfolder prefs for validity.
-
- Returns True if no problem, false otherwise.
- """
- 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
def _init_prefs(self):
@@ -2115,12 +2432,13 @@ class RapidApp(dbus.service.Object):
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)
+ self.setup_devices(on_startup = False, on_preference_change = True, block_auto_start = True)
+ self._set_device_collection_size()
+
if self.main_notebook.get_current_page() == 1: # preview of file
self.main_notebook.set_current_page(0)
@@ -2148,6 +2466,36 @@ class RapidApp(dbus.service.Object):
# Main app window management and setup
# # #
+ def _init_pynotify(self):
+ """
+ Initialize system notification messages
+ """
+
+ if not pynotify.init("TestCaps"):
+ logger.critical("Problem using pynotify.")
+ gtk.main_quit()
+
+ do_not_size_icon = False
+ self.notification_icon_size = 48
+ try:
+ info = pynotify.get_server_info()
+ except:
+ logger.warning("Desktop environment notification server is incorrectly configured.")
+ else:
+ try:
+ if info["name"] == 'notify-osd':
+ do_not_size_icon = True
+ except:
+ pass
+
+ if do_not_size_icon:
+ self.application_icon = gtk.gdk.pixbuf_new_from_file(
+ paths.share_dir('glade3/rapid-photo-downloader.svg'))
+ else:
+ self.application_icon = gtk.gdk.pixbuf_new_from_file_at_size(
+ paths.share_dir('glade3/rapid-photo-downloader.svg'),
+ self.notification_icon_size, self.notification_icon_size)
+
def _init_widgets(self):
"""
Initialize widgets in the main window, and variables that point to them
@@ -2167,6 +2515,7 @@ class RapidApp(dbus.service.Object):
self.next_image_action = builder.get_object("next_image_action")
self.prev_image_action = builder.get_object("prev_image_action")
self.menu_log_window = builder.get_object("menu_log_window")
+ self.speed_label = builder.get_object("speed_label")
# Only enable this action when actually displaying a preview
self.next_image_action.set_sensitive(False)
@@ -2200,9 +2549,17 @@ class RapidApp(dbus.service.Object):
# Download action state
self.download_action_is_download = True
- #job code initialization
- self.last_chosen_job_code = None
- self.prompting_for_job_code = False
+ # Track the time a download commences
+ self.download_start_time = None
+
+ # Whether a system wide notifcation message should be shown
+ # after a download has occurred in parallel
+ self.display_summary_notification = False
+
+ # Values used to display how much longer a download will take
+ self.time_remaining = downloadtracker.TimeRemaining()
+ self.time_check = downloadtracker.TimeCheck()
+
def _set_window_size(self):
"""
@@ -2375,6 +2732,12 @@ class RapidApp(dbus.service.Object):
self.error_log.widget.show()
else:
self.error_log.widget.hide()
+
+ def notify_prefs_are_invalid(self, details):
+ title = _("Program preferences are invalid")
+ logger.critical(title)
+ self.log_error(severity=config.CRITICAL_ERROR, problem=title,
+ details=details)
# # #
@@ -2576,6 +2939,7 @@ class RapidApp(dbus.service.Object):
if conn_type == rpdmp.CONN_COMPLETE:
connection.close()
+ self.scan_manager.no_tasks -= 1
size, file_type_counter, scan_pid = data
size = format_size_for_user(bytes=size)
results_summary, file_types_present = file_type_counter.summarize_file_count()
@@ -2585,13 +2949,20 @@ class RapidApp(dbus.service.Object):
self.device_collection.update_device(scan_pid, size)
self.device_collection.update_progress(scan_pid, 0.0, results_summary, 0)
self.testing_auto_exit_trip_counter += 1
+ self.set_download_action_sensitivity()
+
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:
+ if not self.testing_auto_exit and not self.auto_start_is_on:
self.download_progressbar.set_text(_("Thumbnails"))
self.thumbnails.generate_thumbnails(scan_pid)
- self.set_download_action_sensitivity()
+ elif self.auto_start_is_on:
+ if self.need_job_code_for_naming and not self.job_code:
+ self.get_job_code()
+ else:
+ self.start_download(scan_pid=scan_pid)
+
self.set_thumbnail_sort()
# signal that no more data is coming, finishing io watch for this pipe
@@ -2601,7 +2972,8 @@ class RapidApp(dbus.service.Object):
logger.critical("incoming pipe length is unexpectedly long: %s" % len(data))
else:
for rpd_file in data:
- self.thumbnails.add_file(rpd_file)
+ self.thumbnails.add_file(rpd_file=rpd_file,
+ generate_thumbnail = not self.auto_start_is_on)
# must return True for this method to be called again
return True