summaryrefslogtreecommitdiff
path: root/rapid/rapid.py
diff options
context:
space:
mode:
Diffstat (limited to 'rapid/rapid.py')
-rwxr-xr-xrapid/rapid.py4177
1 files changed, 0 insertions, 4177 deletions
diff --git a/rapid/rapid.py b/rapid/rapid.py
deleted file mode 100755
index 303e1f7..0000000
--- a/rapid/rapid.py
+++ /dev/null
@@ -1,4177 +0,0 @@
-#!/usr/bin/python
-# -*- coding: latin1 -*-
-
-### Copyright (C) 2011-2014 Damon Lynch <damonlynch@gmail.com>
-
-### This program is free software; you can redistribute it and/or modify
-### it under the terms of the GNU General Public License as published by
-### the Free Software Foundation; either version 2 of the License, or
-### (at your option) any later version.
-
-### This program is distributed in the hope that it will be useful,
-### but WITHOUT ANY WARRANTY; without even the implied warranty of
-### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-### GNU General Public License for more details.
-
-### You should have received a copy of the GNU General Public License
-### along with this program; if not, write to the Free Software
-### Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
-### USA
-
-use_pynotify = True
-
-import tempfile
-
-import dbus
-import dbus.bus
-import dbus.service
-from dbus.mainloop.glib import DBusGMainLoop
-DBusGMainLoop(set_as_default=True)
-
-from optparse import OptionParser
-
-import gtk
-import gtk.gdk as gdk
-
-from gobject.constants import G_MAXINT
-
-import webbrowser
-
-import sys, time, types, os, datetime
-
-import gobject, pango, cairo, array, pangocairo, gio
-
-if use_pynotify:
- import pynotify
-
-from multiprocessing import Process, Pipe, Queue, Event, Value, Array, current_process, log_to_stderr
-from ctypes import c_int, c_bool, c_char
-
-import logging
-logger = log_to_stderr()
-
-# Rapid Photo Downloader modules
-
-import rpdfile
-
-from misc import get_folder_selection
-
-import problemnotification as pn
-import thumbnail as tn
-import rpdmultiprocessing as rpdmp
-
-import preferencesdialog
-import prefsrapid
-
-import tableplusminus as tpm
-import generatename as gn
-
-import downloadtracker
-
-import filemodify
-
-from metadatavideo import DOWNLOAD_VIDEO, file_types_to_download
-import metadataphoto
-import metadatavideo
-import metadataexiftool
-
-import scan as scan_process
-import copyfiles
-import subfolderfile
-import backupfile
-from backupfile import PHOTO_BACKUP, VIDEO_BACKUP, PHOTO_VIDEO_BACKUP
-
-import errorlog
-
-import device as dv
-import utilities
-
-import config
-__version__ = config.version
-
-import paths
-
-import gettext
-gettext.bindtextdomain(config.APP_NAME)
-gettext.textdomain(config.APP_NAME)
-
-_ = gettext.gettext
-
-
-from utilities import format_size_for_user
-from utilities import register_iconsets
-
-
-from config import STATUS_CANNOT_DOWNLOAD, STATUS_DOWNLOADED, \
- STATUS_DOWNLOADED_WITH_WARNING, \
- STATUS_DOWNLOAD_FAILED, \
- STATUS_DOWNLOAD_PENDING, \
- STATUS_BACKUP_PROBLEM, \
- STATUS_NOT_DOWNLOADED, \
- STATUS_DOWNLOAD_AND_BACKUP_FAILED, \
- STATUS_WARNING
-
-DOWNLOADED = [STATUS_DOWNLOADED, STATUS_DOWNLOADED_WITH_WARNING, STATUS_BACKUP_PROBLEM]
-
-#Translators: if neccessary, for guidance in how to translate this program, you may see http://damonlynch.net/translate.html
-PROGRAM_NAME = _('Rapid Photo Downloader')
-__version__ = config.version
-
-def date_time_human_readable(date, with_line_break=True):
- if with_line_break:
- return _("%(date)s\n%(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")}
- else:
- return _("%(date)s %(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")}
-
-def date_time_subseconds_human_readable(date, subseconds):
- return _("%(date)s %(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \
- {'date':date.strftime("%x"),
- 'hour':date.strftime("%H"),
- 'minute':date.strftime("%M"),
- 'second':date.strftime("%S"),
- 'subsecond': subseconds}
-
-
-class DeviceCollection(gtk.TreeView):
- """
- TreeView display of devices and how many files have been copied, shown
- immediately under the menu in the main application window.
- """
- def __init__(self, parent_app):
-
- self.parent_app = parent_app
- # device icon & name, size of images on the device (human readable),
- # copy progress (%), copy text, eject button (None if irrelevant),
- # process id, pulse
- self.liststore = gtk.ListStore(gtk.gdk.Pixbuf, str, str, float, str,
- gtk.gdk.Pixbuf, int, int)
- self.map_process_to_row = {}
- self.devices_by_scan_pid = {}
-
- gtk.TreeView.__init__(self, self.liststore)
-
- self.props.enable_search = False
- # make it impossible to select a row
- selection = self.get_selection()
- selection.set_mode(gtk.SELECTION_NONE)
- self.set_headers_visible(False)
-
-
- # Device refers to a thing like a camera, memory card in its reader,
- # external hard drive, Portable Storage Device, etc.
- column0 = gtk.TreeViewColumn(_("Device"))
- pixbuf_renderer = gtk.CellRendererPixbuf()
- pixbuf_renderer.set_padding(2, 0)
- text_renderer = gtk.CellRendererText()
- text_renderer.props.ellipsize = pango.ELLIPSIZE_MIDDLE
- text_renderer.set_fixed_size(160, -1)
- eject_renderer = gtk.CellRendererPixbuf()
- column0.pack_start(pixbuf_renderer, expand=False)
- column0.pack_start(text_renderer, expand=True)
- column0.pack_end(eject_renderer, expand=False)
- column0.add_attribute(pixbuf_renderer, 'pixbuf', 0)
- column0.add_attribute(text_renderer, 'text', 1)
- column0.add_attribute(eject_renderer, 'pixbuf', 5)
- self.append_column(column0)
-
-
- # Size refers to the total size of images on the device, typically in
- # MB or GB
- column1 = gtk.TreeViewColumn(_("Size"), gtk.CellRendererText(), text=2)
- self.append_column(column1)
-
- column2 = gtk.TreeViewColumn(_("Download Progress"),
- gtk.CellRendererProgress(),
- value=3,
- text=4,
- pulse=7)
- self.append_column(column2)
- self.show_all()
-
- icontheme = gtk.icon_theme_get_default()
- try:
- self.eject_pixbuf = icontheme.load_icon('media-eject', 16,
- gtk.ICON_LOOKUP_USE_BUILTIN)
- except:
- self.eject_pixbuf = gtk.gdk.pixbuf_new_from_file(
- paths.share_dir('glade3/media-eject.png'))
-
- self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
- self.connect('button-press-event', self.button_clicked)
-
-
- def add_device(self, process_id, device, progress_bar_text = ''):
-
- # add the row, and get a temporary pointer to the row
- size_files = ''
- progress = 0.0
-
- if device.mount is None:
- eject = None
- else:
- eject = self.eject_pixbuf
-
- self.devices_by_scan_pid[process_id] = device
-
- iter = self.liststore.append((device.get_icon(),
- device.get_name(),
- size_files,
- progress,
- progress_bar_text,
- eject,
- process_id,
- -1))
-
- self._set_process_map(process_id, iter)
-
- # adjust scrolled window height, based on row height and number of ready to start downloads
-
- # please note, at program startup, self.row_height() will be less than it will be when already running
- # e.g. when starting with 3 cards, it could be 18, but when adding 2 cards to the already running program
- # (with one card at startup), it could be 21
- # must account for header row at the top
- row_height = self.get_background_area(0, self.get_column(0))[3] + 1
- height = max(((len(self.map_process_to_row) + 1) * row_height), 24)
- self.parent_app.device_collection_scrolledwindow.set_size_request(-1, height)
-
-
- def update_device(self, process_id, total_size_files):
- """
- Updates the size of the photos and videos on the device, displayed to the user
- """
- if process_id in self.map_process_to_row:
- iter = self._get_process_map(process_id)
- self.liststore.set_value(iter, 2, total_size_files)
- else:
- logger.critical("This device is unknown")
-
- def get_device(self, process_id):
- return self.devices_by_scan_pid.get(process_id)
-
- def remove_device(self, process_id):
- if process_id in self.map_process_to_row:
- iter = self._get_process_map(process_id)
- self.liststore.remove(iter)
- del self.map_process_to_row[process_id]
- del self.devices_by_scan_pid[process_id]
-
- def get_all_displayed_processes(self):
- """
- returns a list of the processes currently being displayed to the user
- """
- return self.map_process_to_row.keys()
-
-
- def _set_process_map(self, process_id, iter):
- """
- convert the temporary iter into a tree reference, which is
- permanent
- """
-
- path = self.liststore.get_path(iter)
- treerowref = gtk.TreeRowReference(self.liststore, path)
- self.map_process_to_row[process_id] = treerowref
-
- def _get_process_map(self, process_id):
- """
- return the tree iter for this process
- """
-
- if process_id in self.map_process_to_row:
- treerowref = self.map_process_to_row[process_id]
- path = treerowref.get_path()
- iter = self.liststore.get_iter(path)
- return iter
- else:
- return None
-
- def update_progress(self, scan_pid, percent_complete, progress_bar_text, bytes_downloaded, pulse=None):
-
- iter = self._get_process_map(scan_pid)
- if iter:
- if percent_complete:
- self.liststore.set_value(iter, 3, percent_complete)
- if progress_bar_text:
- self.liststore.set_value(iter, 4, progress_bar_text)
-
- if pulse is not None:
- if pulse:
- # Make the bar pulse
- self.liststore.set_value(iter, 7, self.liststore.get_value(iter, 7) + 1)
- else:
- # Set to finished state
- self.liststore.set_value(iter, 7, G_MAXINT)
- else:
- # Reset to allow fraction to be set
- self.liststore.set_value(iter, 7, -1)
-
-
-
- def button_clicked(self, widget, event):
- """
- Look for left single click on eject button
- """
- if event.button == 1:
- if len (self.liststore):
- x = int(event.x)
- y = int(event.y)
- path, column, cell_x, cell_y = self.get_path_at_pos(x, y)
- if path is not None:
- if column == self.get_column(0):
- if cell_x >= column.get_width() - self.eject_pixbuf.get_width():
- iter = self.liststore.get_iter(path)
- if self.liststore.get_value(iter, 5) is not None:
- self.unmount(process_id = self.liststore.get_value(iter, 6))
-
- def unmount(self, process_id):
- device = self.devices_by_scan_pid[process_id]
- if device.mount is not None:
- logger.debug("Unmounting device with scan pid %s", process_id)
- device.mount.unmount(self.unmount_callback)
-
-
- def unmount_callback(self, mount, result):
- name = mount.get_name()
-
- try:
- mount.unmount_finish(result)
- logger.debug("%s successfully unmounted" % name)
- except gio.Error, inst:
- logger.error("%s did not unmount: %s", name, inst)
-
- if use_pynotify:
- title = _("%(device)s did not unmount") % {'device': name}
- message = '%s' % inst
-
- n = pynotify.Notification(title, message)
- n.set_icon_from_pixbuf(self.parent_app.application_icon)
- try:
- n.show()
- except:
- logger.error("Unable to display message using notification system")
-
-
-def create_cairo_image_surface(pil_image, image_width, image_height):
- imgd = pil_image.tobytes("raw","BGRA", 0, 1)
- data = array.array('B',imgd)
- stride = image_width * 4
- image = cairo.ImageSurface.create_for_data(data, cairo.FORMAT_ARGB32,
- image_width, image_height, stride)
- return image
-
-class ThumbnailCellRenderer(gtk.CellRenderer):
- __gproperties__ = {
- "image": (gobject.TYPE_PYOBJECT, "Image",
- "Image", gobject.PARAM_READWRITE),
-
- "filename": (gobject.TYPE_STRING, "Filename",
- "Filename", '', gobject.PARAM_READWRITE),
-
- "status": (gtk.gdk.Pixbuf, "Status",
- "Status", gobject.PARAM_READWRITE),
- }
-
- def __init__(self, checkbutton_height):
- gtk.CellRenderer.__init__(self)
- self.image = None
-
- self.image_area_size = 100
- self.text_area_size = 30
- self.padding = 6
- self.checkbutton_height = checkbutton_height
- self.icon_width = 20
-
- def do_set_property(self, pspec, value):
- setattr(self, pspec.name, value)
-
- def do_get_property(self, pspec):
- return getattr(self, pspec.name)
-
- def do_render(self, window, widget, background_area, cell_area, expose_area, flags):
-
- cairo_context = window.cairo_create()
-
- x = cell_area.x
- y = cell_area.y + self.checkbutton_height - 8
- w = cell_area.width
- h = cell_area.height
-
- #constrain operations to cell area, allowing for a 1 pixel border
- #either side
- #~ cairo_context.rectangle(x-1, y-1, w+2, h+2)
- #~ cairo_context.clip()
-
- #fill in the background with dark grey
- #this ensures that a selected cell's fill does not make
- #the text impossible to read
- #~ cairo_context.rectangle(x, y, w, h)
- #~ cairo_context.set_source_rgb(0.267, 0.267, 0.267)
- #~ cairo_context.fill()
-
- #image width and height
- image_w = self.image.size[0]
- image_h = self.image.size[1]
-
- #center the image horizontally
- #bottom align vertically
- #top left and right corners for the image:
- image_x = x + ((w - image_w) / 2)
- image_y = y + self.image_area_size - image_h
-
- #convert PIL image to format suitable for cairo
- image = create_cairo_image_surface(self.image, image_w, image_h)
-
- # draw a light grey border of 1px around the image
- cairo_context.set_source_rgb(0.66, 0.66, 0.66) #light grey, #a9a9a9
- cairo_context.set_line_width(1)
- cairo_context.rectangle(image_x-.5, image_y-.5, image_w+1, image_h+1)
- cairo_context.stroke()
-
- # draw a thin border around each cell
- #~ cairo_context.set_source_rgb(0.33,0.33,0.33)
- #~ cairo_context.rectangle(x, y, w, h)
- #~ cairo_context.stroke()
-
- #place the image
- cairo_context.set_source_surface(image, image_x, image_y)
- cairo_context.paint()
-
- #text
- context = pangocairo.CairoContext(cairo_context)
-
- text_y = y + self.image_area_size + 10
- text_w = w - self.icon_width
- text_x = x + self.icon_width
- #~ context.rectangle(text_x, text_y, text_w, 15)
- #~ context.clip()
-
- layout = context.create_layout()
-
- width = text_w * pango.SCALE
- layout.set_width(width)
-
- layout.set_alignment(pango.ALIGN_CENTER)
- layout.set_ellipsize(pango.ELLIPSIZE_END)
-
- #font color and size
- fg_color = pango.AttrForeground(65535, 65535, 65535, 0, -1)
- font_size = pango.AttrSize(8192, 0, -1) # 8 * 1024 = 8192
- font_family = pango.AttrFamily('sans', 0, -1)
- attr = pango.AttrList()
- attr.insert(fg_color)
- attr.insert(font_size)
- attr.insert(font_family)
- layout.set_attributes(attr)
-
- layout.set_text(self.filename)
-
- context.move_to(text_x, text_y)
- context.show_layout(layout)
-
- #status
- cairo_context.set_source_pixbuf(self.status, x, y + self.image_area_size + 10)
- cairo_context.paint()
-
- def do_get_size(self, widget, cell_area):
- return (0, 0, self.image_area_size, self.image_area_size + self.text_area_size - self.checkbutton_height + 4)
-
-
-gobject.type_register(ThumbnailCellRenderer)
-
-
-class ThumbnailDisplay(gtk.IconView):
- def __init__(self, parent_app):
- gtk.IconView.__init__(self)
- self.set_spacing(0)
- self.set_row_spacing(5)
- self.set_margin(25)
-
- self.set_selection_mode(gtk.SELECTION_MULTIPLE)
- self.connect('selection-changed', self.on_selection_changed)
- self._selected_items = []
-
- self.rapid_app = parent_app
-
- self.batch_size = 10
-
- self.thumbnail_manager = ThumbnailManager(self.thumbnail_results, self.batch_size)
- self.preview_manager = PreviewManager(self.preview_results)
-
- self.treerow_index = {}
- self.process_index = {}
-
- self.rpd_files = {}
-
- self.total_thumbs_to_generate = 0
- self.thumbnails_generated = 0
-
- # dict of scan_pids that are having thumbnails generated
- # value is the thumbnail process id
- # this is needed when terminating thumbnailing early such as when
- # user clicks download before the thumbnailing is finished
- self.generating_thumbnails = {}
-
- self.thumbnails = {}
- self.previews = {}
- self.previews_being_fetched = set()
-
- self.stock_photo_thumbnails = tn.PhotoIcons()
- self.stock_video_thumbnails = tn.VideoIcons()
-
- self.SELECTED_COL = 1
- self.UNIQUE_ID_COL = 2
- self.TIMESTAMP_COL = 4
- self.FILETYPE_COL = 5
- self.CHECKBUTTON_VISIBLE_COL = 6
- self.DOWNLOAD_STATUS_COL = 7
- self.STATUS_ICON_COL = 8
-
- self._create_liststore()
-
- self.clear()
-
- checkbutton = gtk.CellRendererToggle()
- checkbutton.set_radio(False)
- checkbutton.props.activatable = True
- checkbutton.props.xalign = 0.0
- checkbutton.connect('toggled', self.on_checkbutton_toggled)
- self.pack_end(checkbutton, expand=False)
-
- self.add_attribute(checkbutton, "active", 1)
- self.add_attribute(checkbutton, "visible", 6)
-
- checkbutton_size = checkbutton.get_size(self, None)
- checkbutton_height = checkbutton_size[3]
- checkbutton_width = checkbutton_size[2]
-
- image = ThumbnailCellRenderer(checkbutton_height)
- self.pack_start(image, expand=True)
- self.add_attribute(image, "image", 0)
- self.add_attribute(image, "filename", 3)
- self.add_attribute(image, "status", 8)
-
- #set the background color to a darkish grey
- self.modify_base(gtk.STATE_NORMAL, gtk.gdk.Color('#444444'))
-
- self.show_all()
- self._setup_icons()
-
- self.connect('item-activated', self.on_item_activated)
-
- def _create_liststore(self):
- """
- Creates the default list store to hold the icons
- """
- self.liststore = gtk.ListStore(
- gobject.TYPE_PYOBJECT, # 0 PIL thumbnail
- gobject.TYPE_BOOLEAN, # 1 selected or not
- str, # 2 unique id
- str, # 3 file name
- int, # 4 timestamp for sorting, converted float
- int, # 5 file type i.e. photo or video
- gobject.TYPE_BOOLEAN, # 6 visibility of checkbutton
- int, # 7 status of download
- gtk.gdk.Pixbuf, # 8 status icon
- )
-
- def _setup_icons(self):
- # icons to be displayed in status column
-
- size = 16
- # standard icons
- failed = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU)
- self.download_failed_icon = failed.scale_simple(size, size, gtk.gdk.INTERP_HYPER)
- error = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU)
- self.error_icon = error.scale_simple(size, size, gtk.gdk.INTERP_HYPER)
- warning = self.render_icon(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_MENU)
- self.warning_icon = warning.scale_simple(size, size, gtk.gdk.INTERP_HYPER)
-
- # Rapid Photo Downloader specific icons
- self.downloaded_icon = gtk.gdk.pixbuf_new_from_file_at_size(
- paths.share_dir('glade3/rapid-photo-downloader-downloaded.svg'),
- size, size)
- self.download_pending_icon = gtk.gdk.pixbuf_new_from_file_at_size(
- paths.share_dir('glade3/rapid-photo-downloader-download-pending.png'),
- size, size)
- self.downloaded_with_warning_icon = gtk.gdk.pixbuf_new_from_file_at_size(
- paths.share_dir('glade3/rapid-photo-downloader-downloaded-with-warning.svg'),
- size, size)
- self.downloaded_with_error_icon = gtk.gdk.pixbuf_new_from_file_at_size(
- paths.share_dir('glade3/rapid-photo-downloader-downloaded-with-error.svg'),
- size, size)
-
- # make the not yet downloaded icon a transparent square
- self.not_downloaded_icon = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, 16, 16)
- self.not_downloaded_icon.fill(0xffffffff)
- self.not_downloaded_icon = self.not_downloaded_icon.add_alpha(True, chr(255), chr(255), chr(255))
-
- def get_status_icon(self, status):
- """
- Returns the correct icon, based on the status
- """
- if status == STATUS_WARNING:
- status_icon = self.warning_icon
- elif status == STATUS_CANNOT_DOWNLOAD:
- status_icon = self.error_icon
- elif status == STATUS_DOWNLOADED:
- status_icon = self.downloaded_icon
- elif status == STATUS_NOT_DOWNLOADED:
- status_icon = self.not_downloaded_icon
- elif status in [STATUS_DOWNLOADED_WITH_WARNING, STATUS_BACKUP_PROBLEM]:
- status_icon = self.downloaded_with_warning_icon
- elif status in [STATUS_DOWNLOAD_FAILED, STATUS_DOWNLOAD_AND_BACKUP_FAILED]:
- status_icon = self.downloaded_with_error_icon
- elif status == STATUS_DOWNLOAD_PENDING:
- status_icon = self.download_pending_icon
- else:
- logger.critical("FIXME: unknown status: %s", status)
- status_icon = self.not_downloaded_icon
- return status_icon
-
- def sort_by_timestamp(self):
- self.liststore.set_sort_column_id(self.TIMESTAMP_COL, gtk.SORT_ASCENDING)
-
- def on_selection_changed(self, iconview):
- """
- Allow selections by row (and not GTK default by square) when user is
- dragging the mouse or using the keyboard to select
- """
- selections = self.get_selected_items()
- if len(selections) > 1:
- previous_sel = selections[0][0] + 1
- # seleted items list always starts with the highest selected item
- for selection in selections:
- current_sel = selection[0]
- if current_sel <> (previous_sel-1):
- for i in range(previous_sel-1, current_sel, -1):
- self.select_path(i)
- previous_sel = current_sel
- self._selected_items = self.get_selected_items()
-
- def on_checkbutton_toggled(self, cellrenderertoggle, path):
- paths = [p[0] for p in self._selected_items]
- if int(path) not in paths:
- self._selected_items = [path,]
-
- for path in self._selected_items:
- iter = self.liststore.get_iter(path)
- status = self.liststore.get_value(iter, self.DOWNLOAD_STATUS_COL)
- if status == STATUS_NOT_DOWNLOADED:
- self.liststore.set_value(iter, self.SELECTED_COL, not cellrenderertoggle.get_active())
- self.select_path(path)
-
- self.rapid_app.set_download_action_sensitivity()
-
-
- def set_selected(self, unique_id, value):
- iter = self.get_iter_from_unique_id(unique_id)
- self.liststore.set_value(iter, self.SELECTED_COL, value)
-
- def add_file(self, rpd_file, generate_thumbnail):
-
- thumbnail_icon = self.get_stock_icon(rpd_file.file_type)
- unique_id = rpd_file.unique_id
- scan_pid = rpd_file.scan_pid
-
- timestamp = int(rpd_file.modification_time)
-
- iter = self.liststore.append((thumbnail_icon,
- True,
- unique_id,
- rpd_file.display_name,
- timestamp,
- rpd_file.file_type,
- True,
- STATUS_NOT_DOWNLOADED,
- self.not_downloaded_icon
- ))
-
- path = self.liststore.get_path(iter)
- treerowref = gtk.TreeRowReference(self.liststore, path)
-
- if scan_pid in self.process_index:
- self.process_index[scan_pid].append(unique_id)
- else:
- self.process_index[scan_pid] = [unique_id,]
-
- self.treerow_index[unique_id] = treerowref
- self.rpd_files[unique_id] = rpd_file
-
- if generate_thumbnail:
- self.total_thumbs_to_generate += 1
-
- def get_sample_file(self, file_type):
- """Returns an rpd_file for of a given file type, or None if it does
- not exist"""
- for unique_id, rpd_file in self.rpd_files.iteritems():
- if rpd_file.file_type == file_type:
- if rpd_file.status <> STATUS_CANNOT_DOWNLOAD:
- return rpd_file
-
- return None
-
- def get_unique_id_from_iter(self, iter):
- return self.liststore.get_value(iter, 2)
-
- def get_iter_from_unique_id(self, unique_id):
- treerowref = self.treerow_index[unique_id]
- path = treerowref.get_path()
- return self.liststore.get_iter(path)
-
- def on_item_activated(self, iconview, path):
- """
- """
- iter = self.liststore.get_iter(path)
- self.show_preview(iter=iter)
- self.advance_get_preview_image(iter)
-
-
- def _get_preview(self, unique_id, rpd_file):
- if unique_id not in self.previews_being_fetched:
- #check if preview should be from a downloaded file, or the source
- if rpd_file.status in DOWNLOADED:
- file_location = rpd_file.download_full_file_name
- thm_file_name = rpd_file.download_thm_full_name
- else:
- file_location = rpd_file.full_file_name
- thm_file_name = rpd_file.thm_full_name
-
- self.preview_manager.get_preview(unique_id, file_location,
- thm_file_name,
- rpd_file.file_type, size_max=None,)
-
- self.previews_being_fetched.add(unique_id)
-
- def show_preview(self, unique_id=None, iter=None):
- if len(self.liststore):
- if unique_id is not None:
- iter = self.get_iter_from_unique_id(unique_id)
- elif iter is not None:
- unique_id = self.get_unique_id_from_iter(iter)
- else:
- # neither an iter or a unique_id were passed
- # use iter from first selected file
- # if none is selected, choose the first file
- selected = self.get_selected_items()
- if selected:
- path = selected[0]
- else:
- path = 0
- iter = self.liststore.get_iter(path)
- unique_id = self.get_unique_id_from_iter(iter)
-
-
- rpd_file = self.rpd_files[unique_id]
-
- if unique_id in self.previews:
- preview_image = self.previews[unique_id]
- else:
- # request daemon process to get a full size thumbnail
- self._get_preview(unique_id, rpd_file)
- if unique_id in self.thumbnails:
- preview_image = self.thumbnails[unique_id]
- else:
- preview_image = self.get_stock_icon(rpd_file.file_type)
-
- checked = self.liststore.get_value(iter, self.SELECTED_COL)
- include_checkbutton_visible = rpd_file.status == STATUS_NOT_DOWNLOADED
- self.rapid_app.show_preview_image(unique_id, preview_image,
- include_checkbutton_visible, checked)
-
- def _get_next_iter(self, iter):
- iter = self.liststore.iter_next(iter)
- if iter is None:
- iter = self.liststore.get_iter_first()
- return iter
-
- def _get_prev_iter(self, iter):
- row = self.liststore.get_path(iter)[0]
- if row == 0:
- row = len(self.liststore)-1
- else:
- row -= 1
- iter = self.liststore.get_iter(row)
- return iter
-
- def show_next_image(self, unique_id):
- iter = self.get_iter_from_unique_id(unique_id)
- iter = self._get_next_iter(iter)
-
- if iter is not None:
- self.show_preview(iter=iter)
-
- # cache next image
- self.advance_get_preview_image(iter, prev=False, next=True)
-
- def show_prev_image(self, unique_id):
- iter = self.get_iter_from_unique_id(unique_id)
- iter = self._get_prev_iter(iter)
-
- if iter is not None:
- self.show_preview(iter=iter)
-
- # cache next image
- self.advance_get_preview_image(iter, prev=True, next=False)
-
-
- def advance_get_preview_image(self, iter, prev=True, next=True):
- unique_ids = []
- if next:
- next_iter = self._get_next_iter(iter)
- unique_ids.append(self.get_unique_id_from_iter(next_iter))
-
- if prev:
- prev_iter = self._get_prev_iter(iter)
- unique_ids.append(self.get_unique_id_from_iter(prev_iter))
-
- for unique_id in unique_ids:
- if not unique_id in self.previews:
- rpd_file = self.rpd_files[unique_id]
- self._get_preview(unique_id, rpd_file)
-
- def check_all(self, check_all, file_type=None):
- for row in self.liststore:
- if row[self.CHECKBUTTON_VISIBLE_COL]:
- if file_type is not None:
- if row[self.FILETYPE_COL] == file_type:
- row[self.SELECTED_COL] = check_all
- else:
- row[self.SELECTED_COL] = check_all
- self.rapid_app.set_download_action_sensitivity()
-
- def files_are_checked_to_download(self):
- """
- Returns True if there is any file that the user has indicated they
- intend to download, else returns False.
- """
- for row in self.liststore:
- if row[self.SELECTED_COL]:
- rpd_file = self.rpd_files[row[self.UNIQUE_ID_COL]]
- if rpd_file.status not in DOWNLOADED:
- return True
- return False
-
- def get_files_checked_for_download(self, scan_pid):
- """
- Returns a dict of scan ids and associated files the user has indicated
- they want to download
-
- If scan_pid is not None, then returns only those files from that scan_pid
- """
- files = dict()
- if scan_pid is None:
- for row in self.liststore:
- if row[self.SELECTED_COL]:
- rpd_file = self.rpd_files[row[self.UNIQUE_ID_COL]]
- if rpd_file.status not in DOWNLOADED:
- scan_pid = rpd_file.scan_pid
- if scan_pid in files:
- files[scan_pid].append(rpd_file)
- else:
- files[scan_pid] = [rpd_file,]
- else:
- files[scan_pid] = []
- for unique_id in self.process_index[scan_pid]:
- rpd_file = self.rpd_files[unique_id]
- if rpd_file.status not in DOWNLOADED:
- iter = self.get_iter_from_unique_id(unique_id)
- if self.liststore.get_value(iter, self.SELECTED_COL):
- files[scan_pid].append(rpd_file)
- return files
-
- def get_no_files_remaining(self, scan_pid):
- """
- Returns the number of files that have not yet been downloaded for the
- scan_pid
- """
- i = 0
- for unique_id in self.process_index[scan_pid]:
- rpd_file = self.rpd_files[unique_id]
- if rpd_file.status == STATUS_NOT_DOWNLOADED:
- i += 1
- return i
-
- def files_remain_to_download(self):
- """
- Returns True if any files remain that are not downloaded, else returns
- False
- """
- for row in self.liststore:
- if row[self.DOWNLOAD_STATUS_COL] == STATUS_NOT_DOWNLOADED:
- return True
- return False
-
-
- def mark_download_pending(self, files_by_scan_pid):
- """
- Sets status to download pending and updates thumbnails display
- """
- for scan_pid in files_by_scan_pid:
- for rpd_file in files_by_scan_pid[scan_pid]:
- unique_id = rpd_file.unique_id
- self.rpd_files[unique_id].status = STATUS_DOWNLOAD_PENDING
- iter = self.get_iter_from_unique_id(unique_id)
- if not self.rapid_app.auto_start_is_on:
- # don't make the checkbox invisible immediately when on auto start
- # otherwise the box can be rendred at the wrong size, as it is
- # realized after the checkbox has already been made invisible
- self.liststore.set_value(iter, self.CHECKBUTTON_VISIBLE_COL, False)
- self.liststore.set_value(iter, self.SELECTED_COL, False)
- self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, STATUS_DOWNLOAD_PENDING)
- icon = self.get_status_icon(STATUS_DOWNLOAD_PENDING)
- self.liststore.set_value(iter, self.STATUS_ICON_COL, icon)
-
- def select_image(self, unique_id):
- iter = self.get_iter_from_unique_id(unique_id)
- path = self.liststore.get_path(iter)
- self.select_path(path)
- self.scroll_to_path(path, use_align=False, row_align=0.5, col_align=0.5)
-
- def get_stock_icon(self, file_type):
- if file_type == rpdfile.FILE_TYPE_PHOTO:
- return self.stock_photo_thumbnails.stock_thumbnail_image_icon
- else:
- return self.stock_video_thumbnails.stock_thumbnail_image_icon
-
- def update_status_post_download(self, rpd_file):
- iter = self.get_iter_from_unique_id(rpd_file.unique_id)
- self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, rpd_file.status)
- icon = self.get_status_icon(rpd_file.status)
- self.liststore.set_value(iter, self.STATUS_ICON_COL, icon)
- self.liststore.set_value(iter, self.CHECKBUTTON_VISIBLE_COL, False)
- self.rpd_files[rpd_file.unique_id] = rpd_file
-
- def generate_thumbnails(self, scan_pid):
- """Initiate thumbnail generation for files scanned in one process
- """
- if scan_pid in self.process_index:
- rpd_files = [self.rpd_files[unique_id] for unique_id in self.process_index[scan_pid]]
- thumbnail_pid = self.thumbnail_manager.add_task((scan_pid, rpd_files))
- self.generating_thumbnails[scan_pid] = thumbnail_pid
-
- def _set_thumbnail(self, unique_id, icon):
- treerowref = self.treerow_index[unique_id]
- path = treerowref.get_path()
- iter = self.liststore.get_iter(path)
- self.liststore.set(iter, 0, icon)
-
- def update_thumbnail(self, thumbnail_data):
- """
- Takes the generated thumbnail and updates the display
-
- If the thumbnail_data includes a second image, that is used to
- update the thumbnail list using the unique_id
- """
- unique_id = thumbnail_data[0]
- thumbnail_icon = thumbnail_data[1]
-
- if thumbnail_icon is not None:
- # get the thumbnail icon in PIL format
- thumbnail_icon = thumbnail_icon.get_image()
-
- if thumbnail_icon:
- self._set_thumbnail(unique_id, thumbnail_icon)
-
- if len(thumbnail_data) > 2:
- # get the 2nd image in PIL format
- self.thumbnails[unique_id] = thumbnail_data[2].get_image()
-
- def terminate_thumbnail_generation(self, scan_pid):
- """
- Terminates thumbnail generation if thumbnails are currently
- being generated for this scan_pid
- """
-
- if scan_pid in self.generating_thumbnails:
- terminated = True
- self.thumbnail_manager.terminate_process(
- self.generating_thumbnails[scan_pid])
- del self.generating_thumbnails[scan_pid]
-
- if len(self.generating_thumbnails) == 0:
- self._reset_thumbnail_tracking_and_display()
- else:
- terminated = False
-
- return terminated
-
- def mark_thumbnails_needed(self, rpd_files):
- for rpd_file in rpd_files:
- if rpd_file.unique_id not in self.thumbnails:
- rpd_file.generate_thumbnail = True
-
- def _reset_thumbnail_tracking_and_display(self):
- self.rapid_app.download_progressbar.set_fraction(0.0)
- self.rapid_app.download_progressbar.set_text('')
- self.thumbnails_generated = 0
- self.total_thumbs_to_generate = 0
-
- def thumbnail_results(self, source, condition):
- connection = self.thumbnail_manager.get_pipe(source)
-
- conn_type, data = connection.recv()
-
- if conn_type == rpdmp.CONN_COMPLETE:
- scan_pid = data
- del self.generating_thumbnails[scan_pid]
- connection.close()
- return False
- else:
-
- for thumbnail_data in data:
- self.update_thumbnail(thumbnail_data)
-
- self.thumbnails_generated += len(data)
-
- # clear progress bar information if all thumbnails have been
- # extracted
- if self.thumbnails_generated == self.total_thumbs_to_generate:
- self._reset_thumbnail_tracking_and_display()
- else:
- if self.total_thumbs_to_generate:
- self.rapid_app.download_progressbar.set_fraction(
- float(self.thumbnails_generated) / self.total_thumbs_to_generate)
-
-
- return True
-
- def preview_results(self, unique_id, preview_full_size, preview_small):
- """
- Receive a full size preview image and update
- """
- self.previews_being_fetched.remove(unique_id)
- if preview_full_size:
- preview_image = preview_full_size.get_image()
- self.previews[unique_id] = preview_image
- self.rapid_app.update_preview_image(unique_id, preview_image)
-
- # user can turn off option for thumbnail generation after a scan
- if unique_id not in self.thumbnails and preview_small is not None:
- self._set_thumbnail(unique_id, preview_small.get_image())
-
-
- def clear_all(self, scan_pid=None, keep_downloaded_files=False):
- """
- Removes files from display and internal tracking.
-
- If scan_pid is not None, then only files matching that scan_pid will
- be removed. Otherwise, everything will be removed.
-
- If keep_downloaded_files is True, files will not be removed if they
- have been downloaded.
- """
- if scan_pid is None and not keep_downloaded_files:
-
- # Here it is critically important to create a brand new liststore,
- # because the old one is set to be sorted, which is extremely slow.
- logger.debug("Creating new thumbnails model")
- self.set_model(None)
- self._create_liststore()
-
- self.treerow_index = {}
- self.process_index = {}
-
- self.rpd_files = {}
- else:
- if scan_pid in self.process_index:
- for unique_id in self.process_index[scan_pid]:
- rpd_file = self.rpd_files[unique_id]
- if not keep_downloaded_files or not rpd_file.status in DOWNLOADED:
- treerowref = self.treerow_index[rpd_file.unique_id]
- path = treerowref.get_path()
- iter = self.liststore.get_iter(path)
- self.liststore.remove(iter)
- del self.treerow_index[rpd_file.unique_id]
- del self.rpd_files[rpd_file.unique_id]
- if not keep_downloaded_files or not len(self.process_index[scan_pid]):
- del self.process_index[scan_pid]
-
- def display_thumbnails(self):
- self.set_model(self.liststore)
-
-class TaskManager:
- def __init__(self, results_callback, batch_size):
- self.results_callback = results_callback
-
- # List of actual process, it's terminate_queue, and it's run_event
- self._processes = []
-
- self._pipes = {}
- self.batch_size = batch_size
-
- self.paused = False
- self.no_tasks = 0
-
-
- def add_task(self, task):
- pid = self._setup_task(task)
- logger.debug("TaskManager PID: %s", pid)
- self.no_tasks += 1
- return pid
-
-
- def _setup_task(self, task):
- task_results_conn, task_process_conn = self._setup_pipe()
-
- source = task_results_conn.fileno()
- self._pipes[source] = task_results_conn
- gobject.io_add_watch(source, gobject.IO_IN, self.results_callback)
-
- terminate_queue = Queue()
- run_event = Event()
- run_event.set()
-
- return self._initiate_task(task, task_results_conn, task_process_conn,
- terminate_queue, run_event)
-
- def _setup_pipe(self):
- return Pipe(duplex=False)
-
- def _initiate_task(self, task, task_process_conn, terminate_queue, run_event):
- logger.error("Implement child class method!")
-
-
- def processes(self):
- for i in range(len(self._processes)):
- yield self._processes[i]
-
- def start(self):
- self.paused = False
- for scan in self.processes():
- run_event = scan[2]
- if not run_event.is_set():
- run_event.set()
-
- def pause(self):
- self.paused = True
- for scan in self.processes():
- run_event = scan[2]
- if run_event.is_set():
- run_event.clear()
-
- def _terminate_process(self, p):
- self._send_termination_msg(p)
- # The process might be paused: let it run
- run_event = p[2]
- if not run_event.is_set():
- run_event.set()
-
- def _send_termination_msg(self, p):
- p[1].put(None)
-
- def terminate_process(self, process_id):
- """
- Send a signal to process with matching process_id that it should
- immediately terminate
- """
- for p in self.processes():
- if p[0].pid == process_id:
- if p[0].is_alive():
- self._terminate_process(p)
-
- def request_termination(self):
- """
- Send a signal to processes that they should immediately terminate
- """
- requested = False
- for p in self.processes():
- if p[0].is_alive():
- requested = True
- self._terminate_process(p)
-
- return requested
-
- def terminate_forcefully(self):
- """
- Forcefully terminates any running processes. Use with great caution.
- No cleanup action is performed.
-
- As python essential reference (4th edition) says, if the process
- 'holds a lock or is involved with interprocess communication,
- terminating it might cause a deadlock or corrupted I/O.'
- """
-
- for p in self.processes():
- if p[0].is_alive():
- logger.info("Forcefully terminating %s in %s" , p[0].name,
- self.__class__.__name__)
- p[0].terminate()
-
-
- def get_pipe(self, source):
- return self._pipes[source]
-
- def get_no_active_processes(self):
- """
- Returns how many processes are currently active, i.e. running
- """
- i = 0
- for p in self.processes():
- if p[0].is_alive():
- i += 1
- return i
-
-
-class ScanManager(TaskManager):
-
- def __init__(self, results_callback, batch_size,
- add_device_function):
- TaskManager.__init__(self, results_callback, batch_size)
- self.add_device_function = add_device_function
-
- def _initiate_task(self, task, task_results_conn, task_process_conn,
- terminate_queue, run_event):
-
- device = task[0]
- ignored_paths = task[1]
- use_re_ignored_paths = task[2]
-
- scan = scan_process.Scan(device.get_path(),
- ignored_paths,
- use_re_ignored_paths,
- self.batch_size,
- task_process_conn, terminate_queue, run_event)
- scan.start()
- self._processes.append((scan, terminate_queue, run_event))
- self.add_device_function(scan.pid, device,
- # This refers to when a device like a hard drive is having its contents scanned,
- # looking for photos or videos. It is visible initially in the progress bar for each device
- # (which normally holds "x photos and videos").
- # It maybe displayed only briefly if the contents of the device being scanned is small.
- progress_bar_text=_('scanning...'))
-
- return scan.pid
-
-class CopyFilesManager(TaskManager):
-
- def _initiate_task(self, task, task_results_conn,
- task_process_conn, terminate_queue, run_event):
- photo_download_folder = task[0]
- video_download_folder = task[1]
- scan_pid = task[2]
- files = task[3]
- verify_files = task[4]
- modify_files_during_download = task[5]
- modify_pipe = task[6]
-
- copy_files = copyfiles.CopyFiles(photo_download_folder,
- video_download_folder,
- files,
- verify_files,
- modify_files_during_download,
- modify_pipe,
- scan_pid, self.batch_size,
- task_process_conn, terminate_queue, run_event)
- copy_files.start()
- self._processes.append((copy_files, terminate_queue, run_event))
- return copy_files.pid
-
-class ThumbnailManager(TaskManager):
- def _initiate_task(self, task, task_results_conn,
- task_process_conn, terminate_queue, run_event):
- scan_pid = task[0]
- files = task[1]
- generator = tn.GenerateThumbnails(scan_pid, files, self.batch_size,
- task_process_conn, terminate_queue,
- run_event)
- generator.start()
- self._processes.append((generator, terminate_queue, run_event))
- return generator.pid
-
-class FileModifyManager(TaskManager):
- """Handles the modification or verification of of downloaded files before they are renamed
- Duplex, multiprocess, similar to BackupFilesManager
- """
- def __init__(self, results_callback):
- TaskManager.__init__(self, results_callback=results_callback,
- batch_size=0)
- self.file_modify_by_scan_pid = {}
-
- def _initiate_task(self, task, task_results_conn, task_process_conn,
- terminate_queue, run_event):
- scan_pid = task[0]
- auto_rotate_jpeg = task[1]
- focal_length = task[2]
- verify_file = task[3]
- refresh_md5_on_file_change = task[4]
-
- file_modify = filemodify.FileModify(auto_rotate_jpeg, focal_length,
- verify_file, refresh_md5_on_file_change,
- task_process_conn, terminate_queue,
- run_event)
- file_modify.start()
- self._processes.append((file_modify, terminate_queue, run_event,
- task_results_conn))
-
- self.file_modify_by_scan_pid[scan_pid] = (task_results_conn, file_modify.pid)
-
- return file_modify.pid
-
- def _setup_pipe(self):
- return Pipe(duplex=True)
-
- def _send_termination_msg(self, p):
- p[1].put(None)
- p[3].send((None, None))
-
- def get_modify_pipe(self, scan_pid):
- return self.file_modify_by_scan_pid[scan_pid][0]
-
-
-class BackupFilesManager(TaskManager):
- """
- Handles backup processes. This is a little different from some other Task
- Manager classes in that its pipe is Duplex, and the work done by it
- is not pre-assigned when the process is started.
-
- Duplex, multiprocess.
- """
- def __init__(self, results_callback, batch_size):
- TaskManager.__init__(self, results_callback, batch_size)
- self.backup_devices_by_path = {}
-
- def _setup_pipe(self):
- return Pipe(duplex=True)
-
- def _send_termination_msg(self, p):
- p[1].put(None)
- p[3].send((None, None, None, None, None, None, None))
-
- def _initiate_task(self, task, task_results_conn, task_process_conn,
- terminate_queue, run_event):
- path = task[0]
- name = task[1]
- backup_type = task[2]
- backup_files = backupfile.BackupFiles(path, name, self.batch_size,
- task_process_conn, terminate_queue,
- run_event)
- backup_files.start()
- self._processes.append((backup_files, terminate_queue, run_event,
- task_results_conn))
-
- self.backup_devices_by_path[path] = (task_results_conn, backup_files.pid,
- backup_type)
-
- return backup_files.pid
-
- def backup_file(self, move_succeeded, rpd_file, path_suffix,
- backup_duplicate_overwrite,
- verify_file,
- download_count):
-
- if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO:
- logger.debug("Backing up photo %s", rpd_file.download_name)
- else:
- logger.debug("Backing up video %s", rpd_file.download_name)
-
- for path in self.backup_devices_by_path:
- backup_type = self.backup_devices_by_path[path][2]
- do_backup = ((backup_type == PHOTO_VIDEO_BACKUP) or
- (rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO and backup_type == PHOTO_BACKUP) or
- (rpd_file.file_type == rpdfile.FILE_TYPE_VIDEO and backup_type == VIDEO_BACKUP))
- if do_backup:
- logger.debug("Backing up to %s", path)
- else:
- logger.debug("Not backing up to %s", path)
- # Even if not going to backup to this device, need to send it anyway so
- # progress bar can be updated. Not this most efficient but the
- # code is much more simple
- task_results_conn = self.backup_devices_by_path[path][0]
- task_results_conn.send((move_succeeded, do_backup, rpd_file,
- path_suffix,
- backup_duplicate_overwrite,
- verify_file, download_count))
-
- def add_device(self, path, name, backup_type):
- """
- Convenience function to setup adding a backup device
- """
- return self.add_task((path, name, backup_type))
-
- def remove_device(self, path):
- pid = self.backup_devices_by_path[path][1]
- self.terminate_process(pid)
- del self.backup_devices_by_path[path]
-
-
-class SingleInstanceTaskManager:
- """
- Base class to manage single instance processes. Examples are daemon
- processes, but also a non-daemon process that has one simple task.
-
- Core (infrastructure) functionality is implemented in this class.
- Derived classes should implemented functionality to actually implement
- specific tasks.
- """
- def __init__(self, results_callback):
- self.results_callback = results_callback
-
- self.task_results_conn, self.task_process_conn = Pipe(duplex=True)
-
- source = self.task_results_conn.fileno()
- gobject.io_add_watch(source, gobject.IO_IN, self.task_results)
-
-
-class PreviewManager(SingleInstanceTaskManager):
- def __init__(self, results_callback):
- SingleInstanceTaskManager.__init__(self, results_callback)
- self._get_preview = tn.GetPreviewImage(self.task_process_conn)
- self._get_preview.start()
-
- def get_preview(self, unique_id, full_file_name, thm_file_name, file_type, size_max):
- self.task_results_conn.send((unique_id, full_file_name, thm_file_name, file_type, size_max))
-
- def task_results(self, source, condition):
- unique_id, preview_full_size, preview_small = self.task_results_conn.recv()
- self.results_callback(unique_id, preview_full_size, preview_small)
- return True
-
-class SubfolderFileManager(SingleInstanceTaskManager):
- """
- Manages the daemon process that renames files and creates subfolders
- """
- def __init__(self, results_callback, sequence_values):
- SingleInstanceTaskManager.__init__(self, results_callback)
- self._subfolder_file = subfolderfile.SubfolderFile(self.task_process_conn,
- sequence_values)
- self._subfolder_file.start()
- logger.debug("SubfolderFile PID: %s", self._subfolder_file.pid)
-
- def rename_file_and_move_to_subfolder(self, download_succeeded,
- download_count, rpd_file):
-
- logger.debug("Sending file for rename: %s.", download_count)
- self.task_results_conn.send((download_succeeded, download_count,
- rpd_file))
-
-
- def task_results(self, source, condition):
- move_succeeded, rpd_file, download_count = self.task_results_conn.recv()
- self.results_callback(move_succeeded, rpd_file, download_count)
- return True
-
-class ResizblePilImage(gtk.DrawingArea):
- def __init__(self, bg_color=None):
- gtk.DrawingArea.__init__(self)
- self.base_image = None
- self.bg_color = bg_color
- self.connect('expose_event', self.expose)
-
- def set_image(self, image):
- self.base_image = image
-
- #set up sizes and ratio used for drawing the derived image
- self.base_image_w = self.base_image.size[0]
- self.base_image_h = self.base_image.size[1]
- self.base_image_aspect = float(self.base_image_w) / self.base_image_h
-
- self.queue_draw()
-
- def expose(self, widget, event):
-
- cairo_context = self.window.cairo_create()
-
- x = event.area.x
- y = event.area.y
- w = event.area.width
- h = event.area.height
-
- #constrain operations to event area
- cairo_context.rectangle(x, y, w, h)
- cairo_context.clip_preserve()
-
- #set background color, if needed
- if self.bg_color:
- cairo_context.set_source_rgb(*self.bg_color)
- cairo_context.fill_preserve()
-
- if not self.base_image:
- return False
-
- frame_aspect = float(w) / h
-
- if frame_aspect > self.base_image_aspect:
- # Frame is wider than image
- height = h
- width = int(height * self.base_image_aspect)
- else:
- # Frame is taller than image
- width = w
- height = int(width / self.base_image_aspect)
-
- #resize image
- pil_image = self.base_image.copy()
- if self.base_image_w < width or self.base_image_h < height:
- logger.debug("Upsizing image")
- pil_image = tn.upsize_pil(pil_image, (width, height))
- else:
- logger.debug("Downsizing image")
- tn.downsize_pil(pil_image, (width, height))
-
- #image width and height
- image_w = pil_image.size[0]
- image_h = pil_image.size[1]
-
- #center the image horizontally and vertically
- #top left and right corners for the image:
- image_x = x + ((w - image_w) / 2)
- image_y = y + ((h - image_h) / 2)
-
- image = create_cairo_image_surface(pil_image, image_w, image_h)
- cairo_context.set_source_surface(image, image_x, image_y)
- cairo_context.paint()
-
- return False
-
-
-
-class PreviewImage:
-
- def __init__(self, parent_app, builder):
- #set background color to equivalent of '#444444
- self.preview_image = ResizblePilImage(bg_color=(0.267, 0.267, 0.267))
- self.preview_image_eventbox = builder.get_object("preview_eventbox")
- self.preview_image_eventbox.add(self.preview_image)
- self.preview_image.show()
- self.download_this_checkbutton = builder.get_object("download_this_checkbutton")
- self.rapid_app = parent_app
-
- self.base_preview_image = None # large size image used to scale down from
- self.current_preview_size = (0,0)
- self.preview_image_size_limit = (0,0)
-
- self.unique_id = None
-
- def set_preview_image(self, unique_id, pil_image, include_checkbutton_visible=None,
- checked=None):
- """
- """
- self.preview_image.set_image(pil_image)
- self.unique_id = unique_id
- if checked is not None:
- self.download_this_checkbutton.set_active(checked)
- self.download_this_checkbutton.grab_focus()
-
- if include_checkbutton_visible is not None:
- self.download_this_checkbutton.props.visible = include_checkbutton_visible
-
- def update_preview_image(self, unique_id, pil_image):
- if unique_id == self.unique_id:
- self.set_preview_image(unique_id, pil_image)
-
-
-class RapidApp(dbus.service.Object):
- """
- The main Rapid Photo Downloader application class.
-
- Contains functionality for main program window, and directs all other
- processes.
- """
-
- def __init__(self, bus, path, name, taskserver=None, focal_length=None,
- auto_detect=None, device_location=None):
-
- dbus.service.Object.__init__ (self, bus, path, name)
- self.running = False
-
- self.taskserver = taskserver
-
- self.focal_length = focal_length
-
- # Setup program preferences, and set callback for when they change
- self._init_prefs(auto_detect, device_location)
-
- # Initialize widgets in the main window, and variables that point to them
- self._init_widgets()
-
- if use_pynotify:
- self._init_pynotify()
-
- # Initialize job code handling
- self._init_job_code()
-
- # Remember the window size from the last time the program was run, or
- # set a default size
- self._set_window_size()
-
- # Setup various widgets
- self._setup_buttons()
- self._setup_error_icons()
- self._setup_icons()
-
- # Show the main window
- self.rapidapp.show()
-
- # Check program preferences - don't allow auto start if there is a problem
- prefs_valid, msg = prefsrapid.check_prefs_for_validity(self.prefs)
- if not prefs_valid:
- self.notify_prefs_are_invalid(details=msg)
-
- # Initialize variables with which to track important downloads results
- self._init_download_tracking()
-
- # Set up process managers.
- # A task such as scanning a device or copying files is handled in its
- # own process.
- self._start_process_managers()
-
- # Setup devices from which to download from and backup to
- self.setup_devices(on_startup=True, on_preference_change=False,
- block_auto_start=not prefs_valid)
-
- # Ensure the device collection scrolled window is not too small
- self._set_device_collection_size()
-
- def on_rapidapp_destroy(self, widget, data=None):
-
- self._terminate_processes(terminate_file_copies = True)
-
- # save window and component sizes
- self.prefs.vpaned_pos = self.main_vpaned.get_position()
-
- x, y, width, height = self.rapidapp.get_allocation()
- self.prefs.main_window_size_x = width
- self.prefs.main_window_size_y = height
-
- self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker)
-
- gtk.main_quit()
-
- def _terminate_processes(self, terminate_file_copies=False):
-
- if terminate_file_copies:
- logger.info("Terminating all processes...")
-
- scan_termination_requested = self.scan_manager.request_termination()
- thumbnails_termination_requested = self.thumbnails.thumbnail_manager.request_termination()
- backup_termination_requested = self.backup_manager.request_termination()
- file_modify_termination_requested = self.file_modify_manager.request_termination()
-
- if terminate_file_copies:
- copy_files_termination_requested = self.copy_files_manager.request_termination()
- else:
- copy_files_termination_requested = False
-
- if (scan_termination_requested or thumbnails_termination_requested or
- backup_termination_requested or file_modify_termination_requested):
- time.sleep(1)
- if (self.scan_manager.get_no_active_processes() > 0 or
- self.thumbnails.thumbnail_manager.get_no_active_processes() > 0 or
- self.backup_manager.get_no_active_processes() > 0 or
- self.file_modify_manager.get_no_active_processes() > 0):
- time.sleep(1)
- # must try again, just in case a new scan has meanwhile started!
- self.scan_manager.request_termination()
- self.thumbnails.thumbnail_manager.terminate_forcefully()
- self.scan_manager.terminate_forcefully()
- self.backup_manager.terminate_forcefully()
- self.file_modify_manager.terminate_forcefully()
-
- if terminate_file_copies and copy_files_termination_requested:
- time.sleep(1)
- self.copy_files_manager.terminate_forcefully()
-
- if terminate_file_copies:
- self._clean_all_temp_dirs()
-
- # # #
- # Events and tasks related to displaying preview images and thumbnails
- # # #
-
- def on_download_this_checkbutton_toggled(self, checkbutton):
- value = checkbutton.get_active()
- self.thumbnails.set_selected(self.preview_image.unique_id, value)
- self.set_download_action_sensitivity()
-
- def on_preview_eventbox_button_press_event(self, widget, event):
-
- if event.type == gtk.gdk._2BUTTON_PRESS and event.button == 1:
- self.show_thumbnails()
-
- def on_show_thumbnails_action_activate(self, action):
- logger.debug("on_show_thumbnails_action_activate")
- self.show_thumbnails()
-
- def on_show_image_action_activate(self, action):
- logger.debug("on_show_image_action_activate")
- self.thumbnails.show_preview()
-
- def on_check_all_action_activate(self, action):
- self.thumbnails.check_all(check_all=True)
-
- def on_uncheck_all_action_activate(self, action):
- self.thumbnails.check_all(check_all=False)
-
- def on_check_all_photos_action_activate(self, action):
- self.thumbnails.check_all(check_all=True,
- file_type=rpdfile.FILE_TYPE_PHOTO)
-
- def on_check_all_videos_action_activate(self, action):
- self.thumbnails.check_all(check_all=True,
- file_type=rpdfile.FILE_TYPE_VIDEO)
-
- def on_quit_action_activate(self, action):
- self.on_rapidapp_destroy(widget=self.rapidapp, data=None)
-
- def on_refresh_action_activate(self, action):
- self.thumbnails.clear_all()
- self.setup_devices(on_startup=False, on_preference_change=False,
- block_auto_start=True)
-
- def on_get_help_action_activate(self, action):
- webbrowser.open("http://www.damonlynch.net/rapid/help.html")
-
- def on_about_action_activate(self, action):
- self.about.set_property("name", PROGRAM_NAME)
- self.about.set_property("version", utilities.human_readable_version(
- __version__))
- self.about.run()
- self.about.hide()
-
- def on_report_problem_action_activate(self, action):
- webbrowser.open("https://bugs.launchpad.net/rapid")
-
- def on_translate_action_activate(self, action):
- webbrowser.open("http://www.damonlynch.net/rapid/translate.html")
-
- def on_donate_action_activate(self, action):
- webbrowser.open("http://www.damonlynch.net/rapid/donate.html")
-
- def show_preview_image(self, unique_id, image, include_checkbutton_visible, checked):
- if self.main_notebook.get_current_page() == 0: # thumbnails
- logger.debug("Switching to preview image display")
- self.main_notebook.set_current_page(1)
- self.preview_image.set_preview_image(unique_id, image, include_checkbutton_visible, checked)
- self.next_image_action.set_sensitive(True)
- self.prev_image_action.set_sensitive(True)
-
- def update_preview_image(self, unique_id, image):
- self.preview_image.update_preview_image(unique_id, image)
-
- def show_thumbnails(self):
- logger.debug("Switching to thumbnails display")
- self.main_notebook.set_current_page(0)
- self.thumbnails.select_image(self.preview_image.unique_id)
- self.next_image_action.set_sensitive(False)
- self.prev_image_action.set_sensitive(False)
-
-
- def on_next_image_action_activate(self, action):
- if self.preview_image.unique_id is not None:
- self.thumbnails.show_next_image(self.preview_image.unique_id)
-
- def on_prev_image_action_activate(self, action):
- if self.preview_image.unique_id is not None:
- self.thumbnails.show_prev_image(self.preview_image.unique_id)
-
- def display_scan_thumbnails(self):
- """
- If all the scans are complete, sets the sort order and displays
- thumbnails in the icon view
- """
- if self.scan_manager.no_tasks == 0:
- self.thumbnails.sort_by_timestamp()
- self.thumbnails.display_thumbnails()
-
-
- # # #
- # Volume management
- # # #
-
- def start_volume_monitor(self):
- if not self.vmonitor:
- self.vmonitor = gio.volume_monitor_get()
- self.vmonitor.connect("mount-added", self.on_mount_added)
- self.vmonitor.connect("mount-removed", self.on_mount_removed)
-
-
- def _backup_device_name(self, path):
- if self.backup_devices[path][0] is None:
- name = path
- else:
- name = self.backup_devices[path][0].get_name()
- return name
-
- def start_device_scan(self, device):
- """
- Commences the scanning of a device using the preference values for
- any paths to ignore while scanning
- """
- logger.debug("Starting a device scan for device %s", device.get_name())
- return self.scan_manager.add_task([device,
- self.prefs.ignored_paths,
- self.prefs.use_re_ignored_paths])
-
- def confirm_manual_location(self):
- """
- Queries the user to ask if they really want to download from locations
- that could take a very long time to scan. They can choose yes or no.
-
- Returns True if yes or there was no need to ask the user, False if the
- user said no.
- """
- l = self.prefs.device_location
- if l in ['/media', '/run', os.path.expanduser('~'), '/']:
- logger.info("Prompting whether to download from %s", l)
- if l == '/':
- #this location is a human readable explanation for /, and is inserted into Downloading from %(location)s
- l = _('the root of the file system')
- c = preferencesdialog.QuestionDialog(parent_window=self.rapidapp,
- title=_('Rapid Photo Downloader'),
- #message in dialog box which asks the user if they really want to be downloading from this location
- question="<b>" + _("Downloading from %(location)s.") % {'location': l} + "</b>\n\n" +
- _("Do you really want to download from here? On some systems, scanning this location can take a very long time."),
- default_to_yes=False,
- use_markup=True)
- response = c.run()
- user_confirmed = response == gtk.RESPONSE_OK
- c.destroy()
- if not user_confirmed:
- return False
- return True
-
- def setup_devices(self, on_startup, on_preference_change, block_auto_start):
- """
-
- Setup devices from which to download from and backup to
-
- Sets up volumes for downloading from and backing up to
-
- on_startup should be True if the program is still starting,
- i.e. this is being called from the program's initialization.
-
- on_preference_change should be True if this is being called as the
- result of a preference being changed
-
- block_auto_start should be True if automation options to automatically
- start a download should be ignored
-
- Removes any image media that are currently not downloaded,
- or finished downloading
- """
-
- if self.using_volume_monitor():
- self.start_volume_monitor()
-
- self.clear_non_running_downloads()
- if not self.prefs.device_autodetection:
- if not self.confirm_manual_location():
- return
-
- mounts = []
- self.backup_devices = {}
-
- if self.using_volume_monitor():
- # either using automatically detected backup devices
- # or download devices
- for mount in self.vmonitor.get_mounts():
- if not mount.is_shadowed():
- path = mount.get_root().get_path()
- if path:
- if (path in self.prefs.device_blacklist and
- self.search_for_PSD()):
- logger.info("blacklisted device %s ignored", mount.get_name())
- else:
- logger.info("Detected %s", mount.get_name())
- is_backup_mount, backup_file_type = self.check_if_backup_mount(path)
- if is_backup_mount:
- self.backup_devices[path] = (mount, backup_file_type)
- elif (self.prefs.device_autodetection and
- (dv.is_DCIM_device(path) or
- self.search_for_PSD())):
- logger.debug("Appending %s", mount.get_name())
- mounts.append((path, mount))
- else:
- logger.debug("Ignoring %s", mount.get_name())
-
-
- if not self.prefs.device_autodetection:
- # user manually specified the path from which to download
- path = self.prefs.device_location
- if path:
- logger.info("Using manually specified path %s", path)
- if utilities.is_directory(path):
- mounts.append((path, None))
- else:
- logger.error("Download path does not exist: %s", path)
-
- if self.prefs.backup_images:
- if not self.prefs.backup_device_autodetection:
- self._setup_manual_backup()
- self._add_backup_devices()
-
- self.update_no_backup_devices()
-
- # Display amount of free space in a status bar message
- self.display_free_space()
-
- if block_auto_start:
- self.auto_start_is_on = False
- else:
- self.auto_start_is_on = ((not on_preference_change) and
- ((self.prefs.auto_download_at_startup and
- on_startup) or
- (self.prefs.auto_download_upon_device_insertion and
- not on_startup)))
-
- logger.debug("Working with %s devices", len(mounts))
- for m in mounts:
- path, mount = m
- device = dv.Device(path=path, mount=mount)
-
-
- if not self._device_already_detected(device):
- if (self.search_for_PSD() and
- path not in self.prefs.device_whitelist):
- # prompt user to see if device should be used or not
- self.get_use_device(device)
- else:
- scan_pid = self.start_device_scan(device)
- if mount is not None:
- self.mounts_by_path[path] = scan_pid
- if not mounts:
- self.set_download_action_sensitivity()
-
- def _device_already_detected(self, device):
- path = device.get_path()
- if path in self.mounts_by_path:
- logger.debug("Ignoring device %s as already have path %s", device.get_name(), path)
- return True
- else:
- return False
-
- def _setup_manual_backup(self):
- """
- Setup backup devices that the user has manually specified.
- Depending on the folder the user has chosen, the paths for photo and
- video backup will either be the same or they will differ.
- """
- # user manually specified backup locations
- # will backup to these paths, but don't need any volume info
- # associated with them
- self.backup_devices[self.prefs.backup_location] = (None, PHOTO_BACKUP)
- if DOWNLOAD_VIDEO:
- if self.prefs.backup_location <> self.prefs.backup_video_location:
- self.backup_devices[self.prefs.backup_video_location] = (None, VIDEO_BACKUP)
- logger.info("Backing up photos to %s", self.prefs.backup_location)
- logger.info("Backing up videos to %s", self.prefs.backup_video_location)
- else:
- # videos and photos are being backed up to the same location
- self.backup_devices[self.prefs.backup_location] = (None, PHOTO_VIDEO_BACKUP)
- logger.info("Backing up photos and videos to %s", self.prefs.backup_location)
- else:
- logger.info("Backing up photos to %s", self.prefs.backup_location)
-
- def _add_backup_devices(self):
- """
- Add each backup devices / path to backup manager
- """
- for path in self.backup_devices:
- name = self._backup_device_name(path)
- backup_type = self.backup_devices[path][1]
- self.backup_manager.add_device(path, name, backup_type)
-
-
- def get_use_device(self, device):
- """ Prompt user whether or not to download from this device """
-
- logger.info("Prompting whether to use %s", device.get_name())
-
- # On some systems, e.g. Ubuntu 12.10, the GTK/Gnome environment
- # unexpectedly results in a device being added twice and not once.
- # The hack on the next line ensures the user is not prompted twice
- # for the same device.
-
- self.mounts_by_path[device.get_path()] = "PROMPTING"
-
- d = dv.UseDeviceDialog(self.rapidapp, device, self.got_use_device)
-
- def got_use_device(self, dialog, user_selected, permanent_choice, device):
- """ User has chosen whether or not to use a device to download from """
- dialog.destroy()
-
- path = device.get_path()
-
- if user_selected:
- if permanent_choice and path not in self.prefs.device_whitelist:
- # do NOT do a list append operation here without the assignment,
- # or the actual preferences will not be updated!!
- if len(self.prefs.device_whitelist):
- self.prefs.device_whitelist = self.prefs.device_whitelist + [path]
- else:
- self.prefs.device_whitelist = [path]
- scan_pid = self.start_device_scan(device)
- self.mounts_by_path[path] = scan_pid
-
- elif permanent_choice and path not in self.prefs.device_blacklist:
- # do not do a list append operation here without the assignment, or the preferences will not be updated!
- if len(self.prefs.device_blacklist):
- self.prefs.device_blacklist = self.prefs.device_blacklist + [path]
- else:
- self.prefs.device_blacklist = [path]
-
- def search_for_PSD(self):
- """
- Check to see if user preferences are to automatically search for
- Portable Storage Devices or not
- """
- return self.prefs.device_autodetection_psd and self.prefs.device_autodetection
-
- def check_if_backup_mount(self, path):
- """
- Checks to see if backups are enabled and path represents a valid backup
- location. It must be writeable.
-
- Checks against user preferences.
-
- Returns a tuple:
- (True, <backup-type> (one of PHOTO_VIDEO_BACKUP, PHOTO_BACKUP, or VIDEO_BACKUP)) or
- (False, None)
- """
- if self.prefs.backup_images:
- if self.prefs.backup_device_autodetection:
- # Determine if the auto-detected backup device is
- # to be used to backup only photos, or videos, or both.
- # Use the presence of a corresponding directory to
- # determine this.
- # The directory must be writable.
- photo_path = os.path.join(path, self.prefs.backup_identifier)
- p_backup = os.path.isdir(photo_path) and os.access(photo_path, os.W_OK)
- if DOWNLOAD_VIDEO:
- video_path = os.path.join(path, self.prefs.video_backup_identifier)
- v_backup = os.path.isdir(video_path) and os.access(video_path, os.W_OK)
- else:
- v_backup = False
- if p_backup and v_backup:
- logger.info("Photos and videos will be backed up to %s", path)
- return (True, PHOTO_VIDEO_BACKUP)
- elif p_backup:
- logger.info("Photos will be backed up to %s", path)
- return (True, PHOTO_BACKUP)
- elif v_backup:
- logger.info("Videos will be backed up to %s", path)
- return (True, VIDEO_BACKUP)
- elif path == self.prefs.backup_location:
- # user manually specified the path
- if os.access(self.prefs.backup_location, os.W_OK):
- return (True, PHOTO_BACKUP)
- elif path == self.prefs.backup_video_location:
- # user manually specified the path
- if os.access(self.prefs.backup_video_location, os.W_OK):
- return (True, VIDEO_BACKUP)
- return (False, None)
-
- def update_no_backup_devices(self):
- self.no_photo_backup_devices = 0
- self.no_video_backup_devices = 0
- for path, value in self.backup_devices.iteritems():
- backup_type = value[1]
- if backup_type == PHOTO_BACKUP:
- self.no_photo_backup_devices += 1
- elif backup_type == VIDEO_BACKUP:
- self.no_video_backup_devices += 1
- else:
- #both videos and photos are backed up to this device / path
- self.no_photo_backup_devices += 1
- self.no_video_backup_devices += 1
- logger.info("# photo backup devices: %s; # video backup devices: %s",
- self.no_photo_backup_devices, self.no_video_backup_devices)
- self.download_tracker.set_no_backup_devices(self.no_photo_backup_devices,
- self.no_video_backup_devices)
-
- def refresh_backup_media(self):
- """
- Setup the backup media
-
- Assumptions: this is being called after the user has changed their
- preferences AND download media has already been setup
- """
-
- # terminate any running backup processes
- self.backup_manager.request_termination()
-
- self.backup_devices = {}
- if self.prefs.backup_images:
- if not self.prefs.backup_device_autodetection:
- self._setup_manual_backup()
- else:
- for mount in self.vmonitor.get_mounts():
- if not mount.is_shadowed():
- path = mount.get_root().get_path()
- if path:
- is_backup_mount, backup_file_type = self.check_if_backup_mount(path)
- if is_backup_mount:
- # is a backup volume
- if path not in self.backup_devices:
- self.backup_devices[path] = (mount, backup_file_type)
-
- self._add_backup_devices()
-
- self.update_no_backup_devices()
- self.display_free_space()
-
- def using_volume_monitor(self):
- """
- Returns True if programs needs to use gio volume monitor
- """
-
- return (self.prefs.device_autodetection or
- (self.prefs.backup_images and
- self.prefs.backup_device_autodetection
- ))
-
- def on_mount_added(self, vmonitor, mount):
- """
- callback run when gio indicates a new volume
- has been mounted
- """
-
-
- if mount.is_shadowed():
- # ignore this type of mount
- return
-
- path = mount.get_root().get_path()
- if path is not None:
-
- if path in self.prefs.device_blacklist and self.search_for_PSD():
- logger.info("Device %(device)s (%(path)s) ignored" % {
- 'device': mount.get_name(), 'path': path})
- else:
- is_backup_mount, backup_file_type = self.check_if_backup_mount(path)
-
- if is_backup_mount:
- if path not in self.backup_devices:
- self.backup_devices[path] = mount
- name = self._backup_device_name(path)
- self.backup_manager.add_device(path, name, backup_file_type)
- self.update_no_backup_devices()
- self.display_free_space()
-
- elif self.prefs.device_autodetection and (dv.is_DCIM_device(path) or
- self.search_for_PSD()):
-
- self.auto_start_is_on = self.prefs.auto_download_upon_device_insertion
- device = dv.Device(path=path, mount=mount)
-
- if not self._device_already_detected(device):
- if self.search_for_PSD() and path not in self.prefs.device_whitelist:
- # prompt user if device should be used or not
- self.get_use_device(device)
- else:
- scan_pid = self.start_device_scan(device)
- self.mounts_by_path[path] = scan_pid
-
- def on_mount_removed(self, vmonitor, mount):
- """
- callback run when gio indicates a new volume
- has been mounted
- """
-
- path = mount.get_root().get_path()
-
- # three scenarios -
- # the mount has been scanned but downloading has not yet started
- # files are being downloaded from mount (it must be a messy unmount)
- # files have finished downloading from mount
-
- if path in self.mounts_by_path:
- scan_pid = self.mounts_by_path[path]
- del self.mounts_by_path[path]
- # temp directory should be cleaned by finishing of process
-
- self.thumbnails.clear_all(scan_pid = scan_pid,
- keep_downloaded_files = True)
- self.device_collection.remove_device(scan_pid)
-
-
-
- # remove backup volumes
- elif path in self.backup_devices:
- del self.backup_devices[path]
- self.display_free_space()
- self.backup_manager.remove_device(path)
- self.update_no_backup_devices()
-
- # may need to disable download button and menu
- self.set_download_action_sensitivity()
-
- def clear_non_running_downloads(self):
- """
- Clears the display of downloads that are currently not running
- """
-
- # Stop any processes currently scanning or creating thumbnails
- self._terminate_processes(terminate_file_copies=False)
-
- # Remove them from the user interface
- for scan_pid in self.device_collection.get_all_displayed_processes():
- if scan_pid not in self.download_active_by_scan_pid:
- self.device_collection.remove_device(scan_pid)
- self.thumbnails.clear_all(scan_pid=scan_pid)
-
-
-
-
- # # #
- # Download and help buttons, and menu items
- # # #
-
- def on_download_action_activate(self, action):
- """
- Called when a download is activated
- """
-
- if self.copy_files_manager.paused:
- logger.debug("Download resumed")
- self.resume_download()
- else:
- logger.debug("Download activated")
-
- if self.download_action_is_download:
- if self.need_job_code_for_naming and not self.prompting_for_job_code:
- self.get_job_code()
- else:
- self.start_download()
- else:
- self.pause_download()
-
-
- def on_help_action_activate(self, action):
- webbrowser.open("http://www.damonlynch.net/rapid/documentation")
-
- def on_preferences_action_activate(self, action):
-
- preferencesdialog.PreferencesDialog(self)
-
- def set_download_action_sensitivity(self):
- """
- Sets sensitivity of Download action to enable or disable it
-
- Affects download button and menu item
- """
- if not self.download_is_occurring():
- sensitivity = False
- if self.scan_manager.no_tasks == 0:
- if self.thumbnails.files_are_checked_to_download():
- sensitivity = True
-
- self.download_action.set_sensitive(sensitivity)
-
- def set_download_action_label(self, is_download):
- """
- Toggles label betwen pause and download
- """
-
- if is_download:
- self.download_action.set_label(_("Download"))
- self.download_action_is_download = True
- else:
- self.download_action.set_label(_("Pause"))
- self.download_action_is_download = False
-
- # # #
- # Job codes
- # # #
-
-
- def _init_job_code(self):
- self.job_code = self.last_chosen_job_code = ''
- self.need_job_code_for_naming = self.prefs.any_pref_uses_job_code()
- self.prompting_for_job_code = False
-
- def assign_job_code(self, code):
- """ assign job code (which may be empty) to member variable and update user preferences
-
- Update preferences only if code is not empty. Do not duplicate job code.
- """
-
- self.job_code = code
-
- if code:
- #add this value to job codes preferences
- #delete any existing value which is the same
- #(this way it comes to the front, which is where it should be)
- #never modify self.prefs.job_codes in place! (or prefs become screwed up)
-
- jcs = self.prefs.job_codes
- while code in jcs:
- jcs.remove(code)
-
- self.prefs.job_codes = [code] + jcs
-
- def _get_job_code(self, post_job_code_entry_callback):
- """ prompt for a job code """
-
- if not self.prompting_for_job_code:
- logger.debug("Prompting for Job Code")
- self.prompting_for_job_code = True
- j = preferencesdialog.JobCodeDialog(parent_window = self.rapidapp,
- job_codes = self.prefs.job_codes,
- default_job_code = self.last_chosen_job_code,
- post_job_code_entry_callback=post_job_code_entry_callback,
- entry_only = False)
- else:
- logger.debug("Already prompting for Job Code, do not prompt again")
-
- def get_job_code(self):
- self._get_job_code(self.got_job_code)
-
- def got_job_code(self, dialog, user_chose_code, code):
- dialog.destroy()
- self.prompting_for_job_code = False
-
- if user_chose_code:
- if code is None:
- code = ''
- self.assign_job_code(code)
- self.last_chosen_job_code = code
- logger.debug("Job Code %s entered", self.job_code)
- self.start_download()
-
- else:
- # user cancelled
- logger.debug("No Job Code entered")
- self.job_code = ''
- self.auto_start_is_on = False
-
-
- # # #
- # Download
- # # #
-
- def _init_download_tracking(self):
- """
- Initialize variables to track downloads
- """
- # Track download sizes and other values for each device.
- # (Scan id acts as an index to each device. A device could be scanned
- # more than once).
- self.download_tracker = downloadtracker.DownloadTracker()
-
- # Track which temporary directories are created when downloading files
- self.temp_dirs_by_scan_pid = dict()
-
- # Track which downloads are running
- self.download_active_by_scan_pid = []
-
- def modify_files_during_download(self):
- """ Returns True if there is a need to modify or verify files during download"""
- return self.prefs.auto_rotate_jpeg or (self.focal_length is not None) or self.prefs.verify_file
-
-
- def start_download(self, scan_pid=None):
- """
- Start download, renaming and backup of files.
-
- If scan_pid is specified, only files matching it will be downloaded
- """
-
- files_by_scan_pid = self.thumbnails.get_files_checked_for_download(scan_pid)
- self.check_file_types_to_be_downloaded(files_by_scan_pid)
- folders_valid, invalid_dirs = self.check_download_folder_validity(files_by_scan_pid)
-
- if not folders_valid:
- if len(invalid_dirs) > 1:
- msg = _("These download folders are invalid:\n%(folder1)s\n%(folder2)s") % {
- 'folder1': invalid_dirs[0], 'folder2': invalid_dirs[1]}
- else:
- msg = _("This download folder is invalid:\n%s") % invalid_dirs[0]
- self.log_error(config.CRITICAL_ERROR, _("Download cannot proceed"),
- msg)
- else:
- missing_destinations = self.backup_destinations_missing()
- if missing_destinations is not None:
- # Warn user that they have specified that they want to backup a file type, but no such folder exists on backup devices
- if not missing_destinations[0]:
- logger.warning("No backup device contains a valid folder for backing up photos")
- msg = _("No backup device contains a valid folder for backing up %(filetype)s") % {'filetype': _('photos')}
- else:
- logger.warning("No backup device contains a valid folder for backing up videos")
- msg = _("No backup device contains a valid folder for backing up %(filetype)s") % {'filetype': _('videos')}
-
- self.log_error(config.WARNING, _("Backup problem"), msg)
-
- # set time download is starting if it is not already set
- # it is unset when all downloads are completed
- if self.download_start_time is None:
- self.download_start_time = datetime.datetime.now()
-
- # Set status to download pending
- self.thumbnails.mark_download_pending(files_by_scan_pid)
-
- # disable refresh and preferences change while download is occurring
- self.enable_prefs_and_refresh(enabled=False)
-
- for scan_pid in files_by_scan_pid:
- files = files_by_scan_pid[scan_pid]
- # if generating thumbnails for this scan_pid, stop it
- if self.thumbnails.terminate_thumbnail_generation(scan_pid):
- self.thumbnails.mark_thumbnails_needed(files)
-
- self.download_files(files, scan_pid)
-
- self.set_download_action_label(is_download = False)
-
- def pause_download(self):
-
- self.copy_files_manager.pause()
-
- # set action to display Download
- if not self.download_action_is_download:
- self.set_download_action_label(is_download = True)
-
- self.time_check.pause()
-
- def resume_download(self):
- for scan_pid in self.download_active_by_scan_pid:
- self.time_remaining.set_time_mark(scan_pid)
-
- self.time_check.set_download_mark()
-
- self.copy_files_manager.start()
-
- def download_files(self, files, scan_pid):
- """
- Initiate downloading and renaming of files
- """
- # Check which file types will be downloaded for this particular process
- no_photos_to_download = self.files_of_type_present(files,
- rpdfile.FILE_TYPE_PHOTO,
- return_file_count=True)
- if no_photos_to_download:
- photo_download_folder = self.prefs.download_folder
- else:
- photo_download_folder = None
-
- if DOWNLOAD_VIDEO:
- no_videos_to_download = self.files_of_type_present(files,
- rpdfile.FILE_TYPE_VIDEO,
- return_file_count=True)
- if no_videos_to_download:
- video_download_folder = self.prefs.video_download_folder
- else:
- video_download_folder = None
- else:
- video_download_folder = None
- no_videos_to_download = 0
-
- photo_download_size, video_download_size = self.size_files_to_be_downloaded(files)
- self.download_tracker.init_stats(scan_pid=scan_pid,
- photo_size_in_bytes=photo_download_size,
- video_size_in_bytes=video_download_size,
- no_photos_to_download=no_photos_to_download,
- no_videos_to_download=no_videos_to_download)
-
-
- download_size = photo_download_size + video_download_size
-
- if self.prefs.backup_images:
- download_size = download_size + ((self.no_photo_backup_devices * photo_download_size) +
- (self.no_video_backup_devices * video_download_size))
-
- self.time_remaining.set(scan_pid, download_size)
- self.time_check.set_download_mark()
-
- self.download_active_by_scan_pid.append(scan_pid)
-
-
- if len(self.download_active_by_scan_pid) > 1:
- self.display_summary_notification = True
-
- if self.auto_start_is_on and self.prefs.generate_thumbnails:
- for rpd_file in files:
- rpd_file.generate_thumbnail = True
-
- verify_file = self.prefs.verify_file
- if verify_file:
- # since a file might be modified in the file modify process,
- # if it will be backed up, need to refresh the md5 once it has
- # been modified
- refresh_md5_on_file_change = self.prefs.backup_images
- else:
- refresh_md5_on_file_change = False
- modify_files_during_download = self.modify_files_during_download()
- if modify_files_during_download:
- self.file_modify_manager.add_task((scan_pid, self.prefs.auto_rotate_jpeg, self.focal_length, verify_file, refresh_md5_on_file_change))
- modify_pipe = self.file_modify_manager.get_modify_pipe(scan_pid)
- else:
- modify_pipe = None
-
-
- # Initiate copy files process
- self.copy_files_manager.add_task((photo_download_folder,
- video_download_folder, scan_pid,
- files, verify_file,
- modify_files_during_download,
- modify_pipe))
-
- def copy_files_results(self, source, condition):
- """
- Handle results from copy files process
- """
- #FIXME: must handle early termination / pause of copy files process
- connection = self.copy_files_manager.get_pipe(source)
- conn_type, msg_data = connection.recv()
- if conn_type == rpdmp.CONN_PARTIAL:
- msg_type, data = msg_data
-
- if msg_type == rpdmp.MSG_TEMP_DIRS:
- scan_pid, photo_temp_dir, video_temp_dir = data
- self.temp_dirs_by_scan_pid[scan_pid] = (photo_temp_dir, video_temp_dir)
-
- # Report which temporary directories are being used for this
- # download
- if photo_temp_dir and video_temp_dir:
- logger.debug("Using temp dirs %s (photos) & %s (videos)",
- photo_temp_dir, video_temp_dir)
- elif photo_temp_dir:
- logger.debug("Using temp dir %s (photos)",
- photo_temp_dir)
- else:
- logger.debug("Using temp dir %s (videos)",
- video_temp_dir)
- elif msg_type == rpdmp.MSG_BYTES:
- scan_pid, total_downloaded, chunk_downloaded = data
- self.download_tracker.set_total_bytes_copied(scan_pid,
- total_downloaded)
- self.time_check.increment(bytes_downloaded=chunk_downloaded)
- percent_complete = self.download_tracker.get_percent_complete(scan_pid)
- self.device_collection.update_progress(scan_pid, percent_complete,
- None, None)
- self.time_remaining.update(scan_pid, bytes_downloaded=chunk_downloaded)
- elif msg_type == rpdmp.MSG_FILE:
- self.copy_file_results_single_file(data)
-
- return True
- else:
- # Process is complete, i.e. conn_type == rpdmp.CONN_COMPLETE
- connection.close()
- return False
-
-
- def copy_file_results_single_file(self, data):
- """
- Handles results from one of two processes:
- 1. copy_files
- 2. file_modify
-
- Operates after a single file has been copied from the download device
- to the local folder.
-
- Calls the process to rename files and create subfolders (subfolderfile)
- """
-
- download_succeeded, rpd_file, download_count, temp_full_file_name, thumbnail_icon, thumbnail = data
-
- if thumbnail is not None or thumbnail_icon is not None:
- self.thumbnails.update_thumbnail((rpd_file.unique_id,
- thumbnail_icon,
- thumbnail))
-
- self.download_tracker.set_download_count_for_file(
- rpd_file.unique_id, download_count)
- self.download_tracker.set_download_count(
- rpd_file.scan_pid, download_count)
- rpd_file.download_start_time = self.download_start_time
-
- if download_succeeded:
- # Insert preference values needed for name generation
- rpd_file = prefsrapid.insert_pref_lists(self.prefs, rpd_file)
- rpd_file.strip_characters = self.prefs.strip_characters
- rpd_file.download_folder = self.prefs.get_download_folder_for_file_type(rpd_file.file_type)
- rpd_file.download_conflict_resolution = self.prefs.download_conflict_resolution
- rpd_file.synchronize_raw_jpg = self.prefs.must_synchronize_raw_jpg()
- rpd_file.job_code = self.job_code
-
- # Call this even if download did not succeed e.g. file verification error
- self.subfolder_file_manager.rename_file_and_move_to_subfolder(
- download_succeeded,
- download_count,
- rpd_file
- )
-
- def file_modify_results(self, source, condition):
- """
- 'file modify' is a process that runs immediately after 'copy files',
- meaning there can be more than one at one time.
-
- It runs before the renaming process.
- """
- connection = self.file_modify_manager.get_pipe(source)
-
- conn_type, data = connection.recv()
- if conn_type == rpdmp.CONN_PARTIAL:
- self.copy_file_results_single_file(data)
- return True
- else:
- # Process is complete, i.e. conn_type == rpdmp.CONN_COMPLETE
- connection.close()
- return False
-
-
- def download_is_occurring(self):
- """Returns True if a file is currently being downloaded, renamed or
- backed up
- """
- return not len(self.download_active_by_scan_pid) == 0
-
- # # #
- # Create folder and file names for downloaded files
- # # #
-
- def subfolder_file_results(self, move_succeeded, rpd_file, download_count):
- """
- Handle results of subfolder creation and file renaming
- """
-
- scan_pid = rpd_file.scan_pid
- unique_id = rpd_file.unique_id
-
- if rpd_file.status == config.STATUS_DOWNLOADED_WITH_WARNING:
- self.log_error(config.WARNING, rpd_file.error_title,
- rpd_file.error_msg, rpd_file.error_extra_detail)
-
- if self.prefs.backup_images:
- if self.backup_possible(rpd_file.file_type):
- if self.prefs.backup_device_autodetection:
- if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO:
- path_suffix = self.prefs.backup_identifier
- else:
- path_suffix = self.prefs.video_backup_identifier
- else:
- path_suffix = None
-
- self.backup_manager.backup_file(move_succeeded, rpd_file,
- path_suffix,
- self.prefs.backup_duplicate_overwrite,
- self.prefs.verify_file,
- download_count)
- else:
- if rpd_file.status == config.STATUS_DOWNLOAD_FAILED:
- rpd_file.status = config.STATUS_DOWNLOAD_AND_BACKUP_FAILED
- else:
- rpd_file.status = config.STATUS_BACKUP_PROBLEM
-
- self.file_download_finished(move_succeeded, rpd_file)
- else:
- self.file_download_finished(move_succeeded, rpd_file)
-
- def backup_possible(self, file_type):
- if file_type == rpdfile.FILE_TYPE_PHOTO:
- return self.no_photo_backup_devices > 0
- elif file_type == rpdfile.FILE_TYPE_VIDEO:
- return self.no_video_backup_devices > 0
- else:
- logger.critical("Unrecognized file type when determining if backup is possible")
-
-
- def multiple_backup_devices(self, file_type):
- """Returns true if more than one backup device is being used for that
- file type
- """
- return ((file_type == rpdfile.FILE_TYPE_PHOTO and
- self.no_photo_backup_devices > 1) or
- (file_type == rpdfile.FILE_TYPE_VIDEO and
- self.no_video_backup_devices > 1))
-
- def backup_results(self, source, condition):
- """
- Handle results sent from backup processes
- """
- connection = self.backup_manager.get_pipe(source)
- conn_type, msg_data = connection.recv()
- if conn_type == rpdmp.CONN_PARTIAL:
- msg_type, data = msg_data
-
- if msg_type == rpdmp.MSG_BYTES:
- scan_pid, backup_pid, total_downloaded, chunk_downloaded = data
- self.download_tracker.increment_bytes_backed_up(scan_pid,
- chunk_downloaded)
- self.time_check.increment(bytes_downloaded=chunk_downloaded)
- percent_complete = self.download_tracker.get_percent_complete(scan_pid)
- self.device_collection.update_progress(scan_pid, percent_complete,
- None, None)
- self.time_remaining.update(scan_pid, bytes_downloaded=chunk_downloaded)
-
- elif msg_type == rpdmp.MSG_FILE:
- backup_succeeded, do_backup, rpd_file = data
-
- # Only show an error message if there is more than one device
- # backing up files of this type - if that is the case,
- # do not want to rely on showing an error message in the
- # function file_download_finished, as it is only called once,
- # when all files have been backed up
- if not backup_succeeded and self.multiple_backup_devices(rpd_file.file_type) and do_backup:
- self.log_error(config.SERIOUS_ERROR,
- rpd_file.error_title,
- rpd_file.error_msg, rpd_file.error_extra_detail)
-
- if do_backup:
- self.download_tracker.file_backed_up(rpd_file.unique_id)
- if self.download_tracker.all_files_backed_up(rpd_file.unique_id,
- rpd_file.file_type):
- logger.debug("File %s will not be backed up to any more locations", rpd_file.download_name)
- self.file_download_finished(backup_succeeded or not do_backup, rpd_file)
- return True
- else:
- return False
-
-
- def file_download_finished(self, succeeded, rpd_file):
- """
- Called when a file has been downloaded i.e. copied, renamed, and backed up
- """
- scan_pid = rpd_file.scan_pid
- unique_id = rpd_file.unique_id
- # Update error log window if neccessary
- if not succeeded and not self.multiple_backup_devices(rpd_file.file_type):
- self.log_error(config.SERIOUS_ERROR, rpd_file.error_title,
- rpd_file.error_msg, rpd_file.error_extra_detail)
- elif self.prefs.auto_delete:
- # record which files to automatically delete when download
- # completes
- self.download_tracker.add_to_auto_delete(rpd_file)
-
- self.thumbnails.update_status_post_download(rpd_file)
- self.download_tracker.file_downloaded_increment(scan_pid,
- rpd_file.file_type,
- rpd_file.status)
-
- completed, files_remaining = self._update_file_download_device_progress(scan_pid, unique_id, rpd_file.file_type)
-
- if self.download_is_occurring():
- self.update_time_remaining()
-
- if completed:
- # Last file for this scan pid has been downloaded, so clean temp directory
- logger.debug("Purging temp directories")
- self._clean_temp_dirs_for_scan_pid(scan_pid)
- if self.prefs.auto_delete:
- logger.debug("Auto deleting files")
- self.auto_delete(scan_pid)
- self.download_tracker.clear_auto_delete(scan_pid)
- self.download_active_by_scan_pid.remove(scan_pid)
- self.time_remaining.remove(scan_pid)
- self.notify_downloaded_from_device(scan_pid)
- if files_remaining == 0 and self.prefs.auto_unmount:
- self.device_collection.unmount(scan_pid)
-
-
- if not self.download_is_occurring():
- logger.debug("Download completed")
- self.enable_prefs_and_refresh(enabled=True)
- self.notify_download_complete()
- self.download_progressbar.set_fraction(0.0)
-
- self.prefs.stored_sequence_no = self.stored_sequence_value.value
- self.downloads_today_tracker.set_raw_downloads_today_from_int(self.downloads_today_value.value)
- self.downloads_today_tracker.set_raw_downloads_today_date(self.downloads_today_date_value.value)
- self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker)
-
- if ((self.prefs.auto_exit and self.download_tracker.no_errors_or_warnings())
- or self.prefs.auto_exit_force):
- if not self.thumbnails.files_remain_to_download():
- self._terminate_processes()
- gtk.main_quit()
-
- self.download_tracker.purge_all()
- self.speed_label.set_label(" ")
-
- self.display_free_space()
-
- self.set_download_action_label(is_download=True)
- self.set_download_action_sensitivity()
-
- self.job_code = ''
- self.download_start_time = None
-
-
- def update_time_remaining(self):
- update, download_speed = self.time_check.check_for_update()
- if update:
- self.speed_label.set_text(download_speed)
-
- time_remaining = self.time_remaining.time_remaining()
- if time_remaining:
- secs = int(time_remaining)
-
- if secs == 0:
- message = ""
- elif secs == 1:
- message = _("About 1 second remaining")
- elif secs < 60:
- message = _("About %i seconds remaining") % secs
- elif secs == 60:
- message = _("About 1 minute remaining")
- else:
- # Translators: in the text '%(minutes)i:%(seconds)02i', only the : should be translated, if needed.
- # '%(minutes)i' and '%(seconds)02i' should not be modified or left out. They are used to format and display the amount
- # of time the download has remainging, e.g. 'About 5:36 minutes remaining'
- message = _("About %(minutes)i:%(seconds)02i minutes remaining") % {'minutes': secs / 60, 'seconds': secs % 60}
-
- self.rapid_statusbar.pop(self.statusbar_context_id)
- self.rapid_statusbar.push(self.statusbar_context_id, message)
-
- def auto_delete(self, scan_pid):
- """Delete files from download device at completion of download"""
- for file in self.download_tracker.get_files_to_auto_delete(scan_pid):
- f = gio.File(file)
- try:
- f.delete(cancellable=None)
- except gio.Error, inst:
- logger.error("Failure deleting file %s", file)
- logger.error(inst)
-
- def file_types_by_number(self, no_photos, no_videos):
- """
- returns a string to be displayed to the user that can be used
- to show if a value refers to photos or videos or both, or just one
- of each
- """
- if (no_videos > 0) and (no_photos > 0):
- v = _('photos and videos')
- elif (no_videos == 0) and (no_photos == 0):
- v = _('photos or videos')
- elif no_videos > 0:
- if no_videos > 1:
- v = _('videos')
- else:
- v = _('video')
- else:
- if no_photos > 1:
- v = _('photos')
- else:
- v = _('photo')
- return v
-
- def notify_downloaded_from_device(self, scan_pid):
- device = self.device_collection.get_device(scan_pid)
-
- if device.mount is None:
- notification_name = PROGRAM_NAME
- icon = self.application_icon
- else:
- notification_name = device.get_name()
- icon = device.get_icon(self.notification_icon_size)
-
- no_photos_downloaded = self.download_tracker.get_no_files_downloaded(
- scan_pid, rpdfile.FILE_TYPE_PHOTO)
- no_videos_downloaded = self.download_tracker.get_no_files_downloaded(
- scan_pid, rpdfile.FILE_TYPE_VIDEO)
- no_photos_failed = self.download_tracker.get_no_files_failed(
- scan_pid, rpdfile.FILE_TYPE_PHOTO)
- no_videos_failed = self.download_tracker.get_no_files_failed(
- scan_pid, rpdfile.FILE_TYPE_VIDEO)
- no_files_downloaded = no_photos_downloaded + no_videos_downloaded
- no_files_failed = no_photos_failed + no_videos_failed
- no_warnings = self.download_tracker.get_no_warnings(scan_pid)
-
- file_types = self.file_types_by_number(no_photos_downloaded, no_videos_downloaded)
- file_types_failed = self.file_types_by_number(no_photos_failed, no_videos_failed)
- message = _("%(noFiles)s %(filetypes)s downloaded") % \
- {'noFiles':no_files_downloaded, 'filetypes': file_types}
-
- if no_files_failed:
- message += "\n" + _("%(noFiles)s %(filetypes)s failed to download") % {'noFiles':no_files_failed, 'filetypes':file_types_failed}
-
- if no_warnings:
- message = "%s\n%s " % (message, no_warnings) + _("warnings")
-
- if use_pynotify:
- n = pynotify.Notification(notification_name, message)
- n.set_icon_from_pixbuf(icon)
-
- try:
- n.show()
- except:
- logger.error("Unable to display message using notification system")
-
- def notify_download_complete(self):
- if self.display_summary_notification:
- message = _("All downloads complete")
-
- # photo downloads
- photo_downloads = self.download_tracker.total_photos_downloaded
- if photo_downloads:
- filetype = self.file_types_by_number(photo_downloads, 0)
- message += "\n" + _("%(number)s %(numberdownloaded)s") % \
- {'number': photo_downloads,
- 'numberdownloaded': _("%(filetype)s downloaded") % \
- {'filetype': filetype}}
-
- # photo failures
- photo_failures = self.download_tracker.total_photo_failures
- if photo_failures:
- filetype = self.file_types_by_number(photo_failures, 0)
- message += "\n" + _("%(number)s %(numberdownloaded)s") % \
- {'number': photo_failures,
- 'numberdownloaded': _("%(filetype)s failed to download") % \
- {'filetype': filetype}}
-
- # video downloads
- video_downloads = self.download_tracker.total_videos_downloaded
- if video_downloads:
- filetype = self.file_types_by_number(0, video_downloads)
- message += "\n" + _("%(number)s %(numberdownloaded)s") % \
- {'number': video_downloads,
- 'numberdownloaded': _("%(filetype)s downloaded") % \
- {'filetype': filetype}}
-
- # video failures
- video_failures = self.download_tracker.total_video_failures
- if video_failures:
- filetype = self.file_types_by_number(0, video_failures)
- message += "\n" + _("%(number)s %(numberdownloaded)s") % \
- {'number': video_failures,
- 'numberdownloaded': _("%(filetype)s failed to download") % \
- {'filetype': filetype}}
-
- # warnings
- warnings = self.download_tracker.total_warnings
- if warnings:
- message += "\n" + _("%(number)s %(numberdownloaded)s") % \
- {'number': warnings,
- 'numberdownloaded': _("warnings")}
-
- if use_pynotify:
- n = pynotify.Notification(PROGRAM_NAME, message)
- n.set_icon_from_pixbuf(self.application_icon)
- try:
- n.show()
- except:
- logger.error("Unable to display message using notification system")
- self.display_summary_notification = False # don't show it again unless needed
-
-
- def _update_file_download_device_progress(self, scan_pid, unique_id, file_type):
- """
- Increments the progress bar for an individual device
-
- Returns if the download is completed for that scan_pid
- It also returns the number of files remaining for the scan_pid, BUT
- this value is valid ONLY if the download is completed
- """
-
- files_downloaded = self.download_tracker.get_download_count_for_file(unique_id)
- files_to_download = self.download_tracker.get_no_files_in_download(scan_pid)
- file_types = self.download_tracker.get_file_types_present(scan_pid)
- completed = files_downloaded == files_to_download
- if completed and (self.prefs.backup_images and self.backup_possible(file_type)):
- completed = self.download_tracker.all_files_backed_up(unique_id, file_type)
-
- if completed:
- files_remaining = self.thumbnails.get_no_files_remaining(scan_pid)
- else:
- files_remaining = 0
-
- if completed and files_remaining:
- # e.g.: 3 of 205 photos and videos (202 remaining)
- progress_bar_text = _("%(number)s of %(total)s %(filetypes)s (%(remaining)s remaining)") % {
- 'number': files_downloaded,
- 'total': files_to_download,
- 'filetypes': file_types,
- 'remaining': files_remaining}
- else:
- # e.g.: 205 of 205 photos and videos
- progress_bar_text = _("%(number)s of %(total)s %(filetypes)s") % \
- {'number': files_downloaded,
- 'total': files_to_download,
- 'filetypes': file_types}
- percent_complete = self.download_tracker.get_percent_complete(scan_pid)
- self.device_collection.update_progress(scan_pid=scan_pid,
- percent_complete=percent_complete,
- progress_bar_text=progress_bar_text,
- bytes_downloaded=None)
-
- percent_complete = self.download_tracker.get_overall_percent_complete()
- self.download_progressbar.set_fraction(percent_complete)
-
- return (completed, files_remaining)
-
-
- def _clean_all_temp_dirs(self):
- """
- Cleans all temp dirs if they exist
- """
- for scan_pid in self.temp_dirs_by_scan_pid:
- for temp_dir in self.temp_dirs_by_scan_pid[scan_pid]:
- self._purge_dir(temp_dir)
-
- self.temp_dirs_by_scan_pid = {}
-
-
- def _clean_temp_dirs_for_scan_pid(self, scan_pid):
- """
- Deletes temp files and folders used in download
- """
- for temp_dir in self.temp_dirs_by_scan_pid[scan_pid]:
- self._purge_dir(temp_dir)
- del self.temp_dirs_by_scan_pid[scan_pid]
-
- def _purge_dir(self, directory):
- """
- Deletes all files in the directory, and the directory itself.
-
- Does not recursively traverse any subfolders in the directory.
- """
-
- if directory:
- try:
- path = gio.File(directory)
- # first delete any files in the temp directory
- # assume there are no directories in the temp directory
- file_attributes = "standard::name,standard::type"
- children = path.enumerate_children(file_attributes)
- for child in children:
- f = path.get_child(child.get_name())
- f.delete(cancellable=None)
- path.delete(cancellable=None)
- logger.debug("Deleted directory %s", directory)
- except gio.Error, inst:
- logger.error("Failure deleting temporary folder %s", directory)
- logger.error(inst)
-
-
- # # #
- # Preferences
- # # #
-
-
- def _init_prefs(self, auto_detect, device_location):
- self.prefs = prefsrapid.RapidPreferences()
-
- # handle device preferences set from the command line
- # do this before preference changes are handled with notify_add
- if auto_detect:
- self.prefs.device_autodetection = True
- elif device_location:
- self.prefs.device_location = device_location
- self.prefs.device_autodetection = False
-
- self.prefs.notify_add(self.on_preference_changed)
-
- # flag to indicate whether the user changed some preferences that
- # indicate the image and backup devices should be setup again
- self.rerun_setup_available_image_and_video_media = False
- self.rerun_setup_available_backup_media = False
-
- # flag to indicate that the preferences dialog window is being
- # displayed to the user
- self.preferences_dialog_displayed = False
-
- # flag to indicate that the user has modified the download today
- # related values in the preferences dialog window
- self.refresh_downloads_today = False
-
- # these values are used to track the number of backup devices /
- # locations for each file type
- self.no_photo_backup_devices = 0
- self.no_video_backup_devices = 0
-
- self.downloads_today_tracker = self.prefs.get_downloads_today_tracker()
-
- downloads_today = self.downloads_today_tracker.get_and_maybe_reset_downloads_today()
- if downloads_today > 0:
- logger.info("Downloads that have occurred so far today: %s", downloads_today)
- else:
- logger.info("No downloads have occurred so far today")
-
- self.downloads_today_value = Value(c_int,
- self.downloads_today_tracker.get_raw_downloads_today())
- self.downloads_today_date_value = Array(c_char,
- self.downloads_today_tracker.get_raw_downloads_today_date())
- self.day_start_value = Array(c_char,
- self.downloads_today_tracker.get_raw_day_start())
- self.refresh_downloads_today_value = Value(c_bool, False)
- self.stored_sequence_value = Value(c_int, self.prefs.stored_sequence_no)
- self.uses_stored_sequence_no_value = Value(c_bool, self.prefs.any_pref_uses_stored_sequence_no())
- self.uses_session_sequece_no_value = Value(c_bool, self.prefs.any_pref_uses_session_sequece_no())
- self.uses_sequence_letter_value = Value(c_bool, self.prefs.any_pref_uses_sequence_letter_value())
-
- self.check_prefs_upgrade(__version__)
- self.prefs.program_version = __version__
-
- def _check_for_sequence_value_use(self):
- self.uses_stored_sequence_no_value.value = self.prefs.any_pref_uses_stored_sequence_no()
- self.uses_session_sequece_no_value.value = self.prefs.any_pref_uses_session_sequece_no()
- self.uses_sequence_letter_value.value = self.prefs.any_pref_uses_sequence_letter_value()
-
- def check_prefs_upgrade(self, running_version):
- """
- Checks if the running version of the program is different from the
- version recorded in the preferences.
-
- If the version is different, the preferences are checked to see
- whether they should be upgraded or not.
- """
- previous_version = self.prefs.program_version
- if len(previous_version) > 0:
- # the program has been run previously for this user
-
- pv = utilities.pythonify_version(previous_version)
- rv = utilities.pythonify_version(running_version)
-
- if pv <> rv:
- # 0.4.1 and below had only one manual backup location
- # 0.4.2 introduced a distinct video back up location that can be manually set
- # Therefore must duplicate the previous photo & video manual backup location into the
- # new video field, unless it has already been changed already.
-
- if pv < utilities.pythonify_version('0.4.2'):
- if self.prefs.backup_video_location == os.path.expanduser('~'):
- self.prefs.backup_video_location = self.prefs.backup_location
- logger.info("Migrated manual backup location preference to videos: %s",
- self.prefs.backup_video_location)
-
- def on_preference_changed(self, key, value):
- """
- Called when user changes the program's preferences
- """
- logger.debug("Preference change detected: %s", key)
-
- if key == 'show_log_dialog':
- self.menu_log_window.set_active(value)
- elif key in ['device_autodetection', 'device_autodetection_psd',
- 'device_location', 'ignored_paths',
- 'use_re_ignored_paths', 'device_blacklist']:
- self.rerun_setup_available_image_and_video_media = True
- self._set_from_toolbar_state()
- if not self.preferences_dialog_displayed:
- self.post_preference_change()
-
-
- elif key in ['backup_images', 'backup_device_autodetection',
- 'backup_location', 'backup_video_location',
- 'backup_identifier', 'video_backup_identifier']:
- self.rerun_setup_available_backup_media = True
- if not self.preferences_dialog_displayed:
- self.post_preference_change()
-
- # Downloads today and stored sequence numbers are kept in shared memory,
- # so that the subfolderfile daemon process can access and modify them
-
- # Note, totally ignore any changes in downloads today, as it
- # is modified in a special manner via a tracking class
-
- elif key == 'stored_sequence_no':
- if type(value) <> types.IntType:
- logger.critical("Stored sequence number value is malformed")
- else:
- self.stored_sequence_value.value = value
-
- elif key in ['image_rename', 'subfolder', 'video_rename', 'video_subfolder']:
- self.need_job_code_for_naming = self.prefs.any_pref_uses_job_code()
- # Check if stored sequence no is being used
- self._check_for_sequence_value_use()
-
- elif key in ['download_folder', 'video_download_folder']:
- self._set_to_toolbar_values()
- self.display_free_space()
-
- def post_preference_change(self):
- if self.rerun_setup_available_image_and_video_media:
-
- logger.info("Download device settings preferences were changed")
-
- self.thumbnails.clear_all()
- self.setup_devices(on_startup = False, on_preference_change = True, block_auto_start = True)
- self._set_device_collection_size()
-
- if self.main_notebook.get_current_page() == 1: # preview of file
- self.main_notebook.set_current_page(0)
-
- self.rerun_setup_available_image_and_video_media = False
-
- if self.rerun_setup_available_backup_media:
- if self.using_volume_monitor():
- self.start_volume_monitor()
- logger.info("Backup preferences were changed.")
-
- self.refresh_backup_media()
-
- self.rerun_setup_available_backup_media = False
-
- if self.refresh_downloads_today:
- self.downloads_today_value.value = self.downloads_today_tracker.get_raw_downloads_today()
- self.downloads_today_date_value.value = self.downloads_today_tracker.get_raw_downloads_today_date()
- self.day_start_value.value = self.downloads_today_tracker.get_raw_day_start()
- self.refresh_downloads_today_value.value = True
- self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker)
-
-
-
- # # #
- # Main app window management and setup
- # # #
-
- def _init_pynotify(self):
- """
- Initialize system notification messages
- """
-
- if not pynotify.init("TestCaps"):
- logger.warning("There might be problems using pynotify.")
-
- do_not_size_icon = False
- self.notification_icon_size = 48
- try:
- info = pynotify.get_server_info()
- except:
- logger.warning("Desktop environment notification server is incorrectly configured.")
- else:
- try:
- if info["name"] == 'notify-osd':
- do_not_size_icon = True
- except:
- pass
-
- if do_not_size_icon:
- self.application_icon = gtk.gdk.pixbuf_new_from_file(
- paths.share_dir('glade3/rapid-photo-downloader.svg'))
- else:
- self.application_icon = gtk.gdk.pixbuf_new_from_file_at_size(
- paths.share_dir('glade3/rapid-photo-downloader.svg'),
- self.notification_icon_size, self.notification_icon_size)
-
- def _init_widgets(self):
- """
- Initialize widgets in the main window, and variables that point to them
- """
- builder = gtk.Builder()
- builder.set_translation_domain(config.APP_NAME)
- self.builder = builder
- builder.add_from_file(paths.share_dir("glade3/rapid.ui"))
- self.rapidapp = builder.get_object("rapidapp")
- self.from_toolbar = builder.get_object("from_toolbar")
- self.copy_toolbar = builder.get_object("copy_toolbar")
- self.dest_toolbar = builder.get_object("dest_toolbar")
- self.menu_toolbar = builder.get_object("menu_toolbar")
- self.main_vpaned = builder.get_object("main_vpaned")
- self.main_notebook = builder.get_object("main_notebook")
- self.download_action = builder.get_object("download_action")
- self.download_button = builder.get_object("download_button")
-
- self.download_progressbar = builder.get_object("download_progressbar")
- self.rapid_statusbar = builder.get_object("rapid_statusbar")
- self.statusbar_context_id = self.rapid_statusbar.get_context_id("progress")
- self.device_collection_scrolledwindow = builder.get_object("device_collection_scrolledwindow")
- self.next_image_action = builder.get_object("next_image_action")
- self.prev_image_action = builder.get_object("prev_image_action")
- self.menu_log_window = builder.get_object("menu_log_window")
- self.speed_label = builder.get_object("speed_label")
- self.refresh_action = builder.get_object("refresh_action")
- self.preferences_action = builder.get_object("preferences_action")
-
- # Only enable this action when actually displaying a preview
- self.next_image_action.set_sensitive(False)
- self.prev_image_action.set_sensitive(False)
-
- self._init_toolbars()
-
- # About dialog
- builder.add_from_file(paths.share_dir("glade3/about.ui"))
- self.about = builder.get_object("about")
-
- builder.connect_signals(self)
-
- self.preview_image = PreviewImage(self, builder)
-
- thumbnails_scrolledwindow = builder.get_object('thumbnails_scrolledwindow')
- self.thumbnails = ThumbnailDisplay(self)
- thumbnails_scrolledwindow.add(self.thumbnails)
-
- #collection of devices from which to download
- self.device_collection_viewport = builder.get_object("device_collection_viewport")
- self.device_collection = DeviceCollection(self)
- self.device_collection_viewport.add(self.device_collection)
-
- #error log window
- self.error_log = errorlog.ErrorLog(self)
-
- # monitor to handle mounts and dismounts
- self.vmonitor = None
- # track scan ids for mount paths - very useful when a device is unmounted
- self.mounts_by_path = {}
-
- # Download action state
- self.download_action_is_download = True
-
- # Track the time a download commences
- self.download_start_time = None
-
- # Whether a system wide notifcation message should be shown
- # after a download has occurred in parallel
- self.display_summary_notification = False
-
- # Values used to display how much longer a download will take
- self.time_remaining = downloadtracker.TimeRemaining()
- self.time_check = downloadtracker.TimeCheck()
-
- def _init_toolbars(self):
- """ Setup the 3 vertical toolbars on the main screen """
- self._setup_from_toolbar()
- self._setup_copy_move_toolbar()
- self._setup_dest_toolbar()
-
- # size label widths so they are equal, or else the left border of the file chooser will not match
- self.photo_dest_label.realize()
- self._make_widget_widths_equal(self.photo_dest_label, self.video_dest_label)
- self.photo_dest_label.set_alignment(xalign=0.0, yalign=0.5)
- self.video_dest_label.set_alignment(xalign=0.0, yalign=0.5)
-
- # size copy / move buttons so they are equal in length, so arrows align
- self._make_widget_widths_equal(self.copy_button, self.move_button)
-
- def _setup_from_toolbar(self):
- self.from_toolbar.set_style(gtk.TOOLBAR_TEXT)
- self.from_toolbar.set_border_width(5)
-
- from_label = gtk.Label()
- from_label.set_markup("<i>" + _("From") + "</i>")
- self.from_toolbar_label = gtk.ToolItem()
- self.from_toolbar_label.add(from_label)
- self.from_toolbar_label.set_is_important(True)
- self.from_toolbar.insert(self.from_toolbar_label, 0)
-
- self.auto_detect_button = gtk.ToggleToolButton()
- self.auto_detect_button.set_is_important(True)
- self.auto_detect_button.set_label(_("Auto Detect"))
- self.from_toolbar.insert(self.auto_detect_button, 1)
-
- self.from_filechooser_button = gtk.FileChooserButton(
- _("Select a folder containing %(file_types)s") % {'file_types':file_types_to_download()})
- self.from_filechooser_button.set_action(
- gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
-
- self.from_filechooser = gtk.ToolItem()
- self.from_filechooser.set_is_important(True)
- self.from_filechooser.add(self.from_filechooser_button)
- self.from_filechooser.set_expand(True)
- self.from_toolbar.insert(self.from_filechooser, 2)
-
- self._set_from_toolbar_state()
-
- #set events after having initialized the values
- self.auto_detect_button.connect("toggled", self.on_auto_detect_button_toggled_event)
- self.from_filechooser_button.connect("selection-changed",
- self.on_from_filechooser_button_selection_changed)
-
- self.from_toolbar.show_all()
-
- def _setup_copy_move_toolbar(self):
- self.copy_toolbar.set_style(gtk.TOOLBAR_TEXT)
- self.copy_toolbar.set_border_width(5)
-
- copy_move_label = gtk.Label(" ")
- self.copy_move_toolbar_label = gtk.ToolItem()
- self.copy_move_toolbar_label.add(copy_move_label)
- self.copy_move_toolbar_label.set_is_important(True)
- self.copy_toolbar.insert(self.copy_move_toolbar_label, 0)
-
- self.copy_hbox = gtk.HBox()
- self.move_hbox = gtk.HBox()
- self.forward_image = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_SMALL_TOOLBAR)
- self.forward_image2 = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_SMALL_TOOLBAR)
- self.forward_image3 = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_SMALL_TOOLBAR)
- self.forward_image4 = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_SMALL_TOOLBAR)
- self.forward_label = gtk.Label(" ")
- self.forward_label2 = gtk.Label(" ")
- self.forward_label3 = gtk.Label(" ")
- self.forward_label4 = gtk.Label(" ")
-
- self.copy_button = gtk.RadioToolButton()
- self.copy_button.set_label(_("Copy"))
- self.copy_button.set_is_important(True)
-
- self.copy_hbox.pack_start(self.forward_label)
- self.copy_hbox.pack_start(self.forward_image)
- self.copy_hbox.pack_start(self.copy_button, expand=False, fill=False)
- self.copy_hbox.pack_start(self.forward_image2)
- self.copy_hbox.pack_start(self.forward_label2)
- copy_box = gtk.ToolItem()
- copy_box.add(self.copy_hbox)
- self.copy_toolbar.insert(copy_box, 1)
-
- self.move_button = gtk.RadioToolButton(self.copy_button)
- self.move_button.set_label(_("Move"))
- self.move_button.set_is_important(True)
- self.move_hbox.pack_start(self.forward_label3)
- self.move_hbox.pack_start(self.forward_image3)
- self.move_hbox.pack_start(self.move_button, expand=False, fill=False)
- self.move_hbox.pack_start(self.forward_image4)
- self.move_hbox.pack_start(self.forward_label4)
- move_box = gtk.ToolItem()
- move_box.add(self.move_hbox)
- self.copy_toolbar.insert(move_box, 2)
-
- self.move_button.set_active(self.prefs.auto_delete)
- self.copy_button.connect("toggled", self.on_copy_button_toggle_event)
-
- self.copy_toolbar.show_all()
- self._set_copy_toolbar_active_arrows()
-
- def _setup_dest_toolbar(self):
- #Destination Toolbar
- self.dest_toolbar.set_border_width(5)
-
- dest_label = gtk.Label()
- dest_label.set_markup("<i>" + _("To") + "</i>")
- self.dest_toolbar_label = gtk.ToolItem()
- self.dest_toolbar_label.add(dest_label)
- self.dest_toolbar_label.set_is_important(True)
- self.dest_toolbar.insert(self.dest_toolbar_label, 0)
-
- photo_dest_hbox = gtk.HBox()
- self.photo_dest_label = gtk.Label(_("Photos:"))
-
- self.to_photo_filechooser_button = gtk.FileChooserButton(
- _("Select a folder to download photos to"))
- self.to_photo_filechooser_button.set_action(
- gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
- photo_dest_hbox.pack_start(self.photo_dest_label, expand=False, fill=False, padding=6)
- photo_dest_hbox.pack_start(self.to_photo_filechooser_button)
- self.to_photo_filechooser = gtk.ToolItem()
- self.to_photo_filechooser.set_is_important(True)
- self.to_photo_filechooser.set_expand(True)
- self.to_photo_filechooser.add(photo_dest_hbox)
- self.dest_toolbar.insert(self.to_photo_filechooser, 1)
-
- video_dest_hbox = gtk.HBox()
- self.video_dest_label = gtk.Label(_("Videos:"))
- self.to_video_filechooser_button = gtk.FileChooserButton(
- _("Select a folder to download videos to"))
- self.to_video_filechooser_button.set_action(
- gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
- video_dest_hbox.pack_start(self.video_dest_label, expand=False, fill=False, padding=6)
- video_dest_hbox.pack_start(self.to_video_filechooser_button)
- self.to_video_filechooser = gtk.ToolItem()
- self.to_video_filechooser.set_is_important(True)
- self.to_video_filechooser.set_expand(True)
- self.to_video_filechooser.add(video_dest_hbox)
- self.dest_toolbar.insert(self.to_video_filechooser, 2)
-
- self._set_to_toolbar_values()
- self.to_photo_filechooser_button.connect("selection-changed",
- self.on_to_photo_filechooser_button_selection_changed)
- #~ self.to_photo_filechooser_button.connect("file-set",
- #~ self.on_to_photo_filechooser_button_file_set)
- #~ self.to_photo_filechooser_button.connect("current-folder-changed",
- #~ self.on_to_photo_filechooser_button_current_folder_changed)
- self.to_video_filechooser_button.connect("selection-changed",
- self.on_to_video_filechooser_button_selection_changed)
- self.dest_toolbar.show_all()
-
- def _make_widget_widths_equal(self, widget1, widget2):
- """takes two widgets and sets a width for both equal to widest one"""
-
- x1, y1, w1, h1 = widget1.get_allocation()
- x2, y2, w2, h2 = widget2.get_allocation()
- w = max(w1, w2)
- h = max(h1, h2)
- widget1.set_size_request(w,h)
- widget2.set_size_request(w,h)
-
- def _set_copy_toolbar_active_arrows(self):
- if self.copy_button.get_active():
- self.forward_image.set_visible(True)
- self.forward_image2.set_visible(True)
- self.forward_image3.set_visible(False)
- self.forward_image4.set_visible(False)
- self.forward_label.set_visible(False)
- self.forward_label2.set_visible(False)
- self.forward_label3.set_visible(True)
- self.forward_label4.set_visible(True)
- else:
- self.forward_image.set_visible(False)
- self.forward_image2.set_visible(False)
- self.forward_image3.set_visible(True)
- self.forward_image4.set_visible(True)
- self.forward_label.set_visible(True)
- self.forward_label2.set_visible(True)
- self.forward_label3.set_visible(False)
- self.forward_label4.set_visible(False)
-
- def on_copy_button_toggle_event(self, radio_button):
- self._set_copy_toolbar_active_arrows()
- self.prefs.auto_delete = not self.copy_button.get_active()
-
- def _set_from_toolbar_state(self):
- logger.debug("_set_from_toolbar_state")
- self.auto_detect_button.set_active(self.prefs.device_autodetection)
- if self.prefs.device_autodetection:
- self.from_filechooser_button.set_sensitive(False)
- self.from_filechooser_button.set_current_folder(self.prefs.device_location)
-
- def on_auto_detect_button_toggled_event(self, button):
- logger.debug("on_auto_detect_button_toggled_event")
- self.from_filechooser_button.set_sensitive(not button.get_active())
- if not self.rerun_setup_available_image_and_video_media:
- self.prefs.device_autodetection = button.get_active()
-
- def on_from_filechooser_button_selection_changed(self, filechooserbutton):
- logger.debug("on_from_filechooser_button_selection_changed")
- path = get_folder_selection(filechooserbutton)
- if path and not self.rerun_setup_available_image_and_video_media:
- self.prefs.device_location = path
-
- def on_from_filechooser_button_file_set(self, button):
- logger.debug("on_from_filechooser_button_file_set")
-
- def on_to_photo_filechooser_button_file_set(self, filechooserbutton):
- logger.debug("on_to_filechooser_button_file_set")
-
- def on_to_photo_filechooser_button_selection_changed(self, filechooserbutton):
- logger.debug("on_to_filechooser_button_selection_changed")
- path = get_folder_selection(filechooserbutton)
- #~ logger.debug("Path: %s", path)
- if path:
- self.prefs.download_folder = path
-
- def on_to_photo_filechooser_button_current_folder_changed(self, filechooserbutton):
- logger.debug("on_to_photo_filechooser_button_current_folder_changed")
-
- def on_to_video_filechooser_button_selection_changed(self, filechooserbutton):
- path = get_folder_selection(filechooserbutton)
- if path:
- self.prefs.video_download_folder = path
-
- def _set_to_toolbar_values(self):
- self.to_photo_filechooser_button.set_current_folder(self.prefs.download_folder)
- self.to_video_filechooser_button.set_current_folder(self.prefs.video_download_folder)
-
- def toolbar_event(self, widget, toolbar):
- pass
-
-
-
- def _set_window_size(self):
- """
- Remember the window size from the last time the program was run, or
- set a default size
- """
-
- if self.prefs.main_window_maximized:
- self.rapidapp.maximize()
- self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH,
- config.DEFAULT_WINDOW_HEIGHT)
- elif self.prefs.main_window_size_x > 0:
- self.rapidapp.set_default_size(self.prefs.main_window_size_x, self.prefs.main_window_size_y)
- else:
- # set a default size
- self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH,
- config.DEFAULT_WINDOW_HEIGHT)
-
-
- def _set_device_collection_size(self):
- """
- Set the size of the device collection scrolled window widget
- """
- if self.device_collection.map_process_to_row:
- height = max(self.device_collection_viewport.size_request()[1], 24)
- self.device_collection_scrolledwindow.set_size_request(-1, height)
- self.main_vpaned.set_position(height)
- else:
- # don't allow the media collection to be absolutely empty
- self.device_collection_scrolledwindow.set_size_request(-1, 47)
-
-
- def on_rapidapp_window_state_event(self, widget, event):
- """ Records the window maximization state in the preferences."""
-
- if event.changed_mask & gdk.WINDOW_STATE_MAXIMIZED:
- self.prefs.main_window_maximized = event.new_window_state & gdk.WINDOW_STATE_MAXIMIZED
-
- def _setup_buttons(self):
- thumbnails_button = self.builder.get_object("thumbnails_button")
- image = gtk.image_new_from_file(paths.share_dir('glade3/thumbnails_icon.png'))
- thumbnails_button.set_image(image)
-
- preview_button = self.builder.get_object("preview_button")
- image = gtk.image_new_from_file(paths.share_dir('glade3/photo_icon.png'))
- preview_button.set_image(image)
-
- next_image_button = self.builder.get_object("next_image_button")
- image = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_BUTTON)
- next_image_button.set_image(image)
-
- prev_image_button = self.builder.get_object("prev_image_button")
- image = gtk.image_new_from_stock(gtk.STOCK_GO_BACK, gtk.ICON_SIZE_BUTTON)
- prev_image_button.set_image(image)
-
- def _setup_icons(self):
- icons = ['rapid-photo-downloader-jobcode',]
- icon_list = [(icon, paths.share_dir('glade3/%s.svg' % icon)) for icon in icons]
- register_iconsets(icon_list)
-
- def _setup_error_icons(self):
- """
- hide display of warning and error symbols in the taskbar until they
- are needed
- """
- self.error_image = self.builder.get_object("error_image")
- self.warning_image = self.builder.get_object("warning_image")
- self.warning_vseparator = self.builder.get_object("warning_vseparator")
- self.error_image.hide()
- self.warning_image.hide()
- self.warning_vseparator.hide()
-
- def enable_prefs_and_refresh(self, enabled):
- """
- If enable is true, then the user is able to activate the preferences
- or refresh command.
- The intention is to be able to disable this during a download
- """
- self.refresh_action.set_sensitive(enabled)
- self.preferences_action.set_sensitive(enabled)
-
- def statusbar_message(self, msg):
- self.rapid_statusbar.push(self.statusbar_context_id, msg)
-
- def statusbar_message_remove(self):
- self.rapid_statusbar.pop(self.statusbar_context_id)
-
- def display_backup_mounts(self):
- """
- Create a message to be displayed to the user showing which backup
- mounts will be used
- """
- message = ''
-
- paths = self.backup_devices.keys()
- i = 0
- v = len(paths)
- prefix = ''
- for b in paths:
- if v > 1:
- if i < (v -1) and i > 0:
- prefix = ', '
- elif i == (v - 1) :
- prefix = " " + _("and") + " "
- i += 1
- message = "%s%s'%s'" % (message, prefix, self.backup_devices[b][0].get_name())
-
- if v > 1:
- message = _("Using backup devices") + " %s" % message
- elif v == 1:
- message = _("Using backup device") + " %s" % message
- else:
- message = _("No backup devices detected")
-
- return message
-
- def display_free_space(self):
- """
- Displays the amount of space free on the filesystem the files will be
- downloaded to.
-
- Also displays backup volumes / path being used.
- """
- photo_dir = self.is_valid_download_dir(path=self.prefs.download_folder, is_photo_dir=True, show_error_in_log=True)
- video_dir = self.is_valid_download_dir(path=self.prefs.video_download_folder, is_photo_dir=False, show_error_in_log=True)
- if photo_dir and video_dir:
- same_file_system = self.same_file_system(self.prefs.download_folder,
- self.prefs.video_download_folder)
- else:
- same_file_system = False
-
- dirs = []
- if photo_dir:
- dirs.append((self.prefs.download_folder, _("photos")))
- if video_dir and not same_file_system:
- dirs.append((self.prefs.video_download_folder, _("videos")))
-
- msg = ''
- if len(dirs) > 1:
- msg = ' ' + _('Free space:') + ' '
-
- for i in range(len(dirs)):
- dir_info = dirs[i]
- folder = gio.File(dir_info[0])
- file_info = folder.query_filesystem_info(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE)
- size = file_info.get_attribute_uint64(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE)
- free = format_size_for_user(bytes=size)
- if len(dirs) > 1:
- #(videos) or (photos) will be appended to the free space message displayed to the
- #user in the status bar.
- #you should only translate this if your language does not use parantheses
- file_type = _("(%(file_type)s)") % {'file_type': dir_info[1]}
-
- #Freespace available on the filesystem for downloading to
- #Displayed in status bar message on main window
- msg += _("%(free)s %(file_type)s") % {'free': free, 'file_type': file_type}
- if i == 0:
- #Inserted in the middle of the statusbar message concerning the amount of freespace
- #Used to differentiate between two different file systems
- #e.g. Free space: 21.3GB (photos); 14.7GB (videos).
- msg += _("; ")
- elif not self.prefs.backup_images:
- #Inserted at the end of the statusbar message concerning the amount of freespace
- #Used to differentiate between two different file systems
- #e.g. Free space: 21.3GB (photos); 14.7GB (videos).
- msg += _(".")
-
- else:
- #Freespace available on the filesystem for downloading to
- #Displayed in status bar message on main window
- #e.g. 14.7GB available
- msg = " " + _("%(free)s free") % {'free': free}
-
-
- if self.prefs.backup_images:
- if not self.prefs.backup_device_autodetection:
- if self.prefs.backup_location == self.prefs.backup_video_location:
- if DOWNLOAD_VIDEO:
- # user manually specified the same location for photos and video backups
- msg2 = _('Backing up photos and videos to %(path)s') % {'path':self.prefs.backup_location}
- else:
- # user manually specified backup location
- msg2 = _('Backing up to %(path)s') % {'path':self.prefs.backup_location}
- else:
- # user manually specified different locations for photo and video backups
- msg2 = _('Backing up photos to %(path)s and videos to %(path2)s') % {
- 'path':self.prefs.backup_location,
- 'path2': self.prefs.backup_video_location}
- else:
- msg2 = self.display_backup_mounts()
-
- if msg:
- msg = _("%(freespace)s. %(backuppaths)s.") % {'freespace': msg, 'backuppaths': msg2}
- else:
- msg = msg2
-
- msg = msg.rstrip()
-
- self.statusbar_message(msg)
-
- def log_error(self, severity, problem, details, extra_detail=None):
- """
- Display error and warning messages to user in log window
- """
- self.error_log.add_message(severity, problem, details, extra_detail)
-
-
- def on_error_eventbox_button_press_event(self, widget, event):
- self.prefs.show_log_dialog = True
- self.error_log.widget.show()
-
-
- def on_menu_log_window_toggled(self, widget):
- active = widget.get_active()
- self.prefs.show_log_dialog = active
- if active:
- self.error_log.widget.show()
- else:
- self.error_log.widget.hide()
-
- def notify_prefs_are_invalid(self, details):
- title = _("Program preferences are invalid")
- logger.critical(title)
- self.log_error(severity=config.CRITICAL_ERROR, problem=title,
- details=details)
-
-
- # # #
- # Utility functions
- # # #
-
- def files_of_type_present(self, files, file_type, return_file_count=False):
- """
- Returns true if there is at least one instance of the file_type
- in the list of files to be copied
-
- If return_file_count is True, then the number of files of that type
- will be counted and returned instead of True or False
- """
- i = 0
- for rpd_file in files:
- if rpd_file.file_type == file_type:
- if return_file_count:
- i += 1
- else:
- return True
- if not return_file_count:
- return False
- else:
- return i
-
- def size_files_to_be_downloaded(self, files):
- """
- Returns the total sizes of the photos and videos to be downloaded in bytes
- """
- photo_size = 0
- video_size = 0
- for rpd_file in files:
- if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO:
- photo_size += rpd_file.size
- else:
- video_size += rpd_file.size
-
- return (photo_size, video_size)
-
- def check_file_types_to_be_downloaded(self, files_by_scan_pid):
- """Determines what types of files need to be downloaded, setting
- self.downloading_photos and self.downloading_videos accordingly"""
- self.downloading_photos = False
- self.downloading_videos = False
- while not self.downloading_photos and not self.downloading_videos:
- for scan_pid in files_by_scan_pid:
- files = files_by_scan_pid[scan_pid]
- if not self.downloading_photos:
- if self.files_of_type_present(files, rpdfile.FILE_TYPE_PHOTO):
- self.downloading_photos = True
- if not self.downloading_videos:
- if self.files_of_type_present(files, rpdfile.FILE_TYPE_VIDEO):
- self.downloading_videos = True
-
-
- def check_download_folder_validity(self, files_by_scan_pid):
- """
- Checks validity of download folders based on the file types the user
- is attempting to download.
-
- If valid, returns a tuple of True and an empty list.
- If invalid, returns a tuple of False and a list of the invalid directores.
- """
- valid = True
- invalid_dirs = []
-
- if self.downloading_photos:
- if not self.is_valid_download_dir(self.prefs.download_folder,
- is_photo_dir=True):
- valid = False
- invalid_dirs.append(self.prefs.download_folder)
- else:
- logger.debug("Photo download folder is valid: %s",
- self.prefs.download_folder)
-
- if self.downloading_videos:
- if not self.is_valid_download_dir(self.prefs.video_download_folder,
- is_photo_dir=False):
- valid = False
- invalid_dirs.append(self.prefs.video_download_folder)
- else:
- logger.debug("Video download folder is valid: %s",
- self.prefs.video_download_folder)
-
-
- return (valid, invalid_dirs)
-
- def backup_destinations_missing(self):
- if self.prefs.backup_images and self.prefs.backup_device_autodetection:
- photo_backup_ok = video_backup_ok = True
- if self.downloading_photos and not self.backup_possible(rpdfile.FILE_TYPE_PHOTO):
- photo_backup_ok = False
- if self.downloading_videos and not self.backup_possible(rpdfile.FILE_TYPE_VIDEO):
- video_backup_ok = False
- if photo_backup_ok and video_backup_ok:
- return None
- else:
- return (photo_backup_ok, video_backup_ok)
- return None
-
- def same_file_system(self, file1, file2):
- """Returns True if the files / diretories are on the same file system
- """
- f1 = gio.File(file1)
- f2 = gio.File(file2)
- f1_info = f1.query_info(gio.FILE_ATTRIBUTE_ID_FILESYSTEM)
- f1_id = f1_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILESYSTEM)
- f2_info = f2.query_info(gio.FILE_ATTRIBUTE_ID_FILESYSTEM)
- f2_id = f2_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILESYSTEM)
- return f1_id == f2_id
-
-
- def same_file(self, file1, file2):
- """Returns True if the files / directories are the same
- """
- f1 = gio.File(file1)
- f2 = gio.File(file2)
-
- file_attributes = "id::file"
- f1_info = f1.query_filesystem_info(file_attributes)
- f1_id = f1_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILE)
- f2_info = f2.query_filesystem_info(file_attributes)
- f2_id = f2_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILE)
- return f1_id == f2_id
-
- def is_valid_download_dir(self, path, is_photo_dir, show_error_in_log=False):
- """
- Checks the following conditions:
- Does the directory exist?
- Is it writable?
-
- if show_error_in_log is True, then display warning in log window, using
- is_photo_dir, which if true means the download directory is for photos,
- if false, for Videos
- """
- valid = False
- if is_photo_dir:
- download_folder_type = _("Photo")
- else:
- download_folder_type = _("Video")
-
- try:
- d = gio.File(path)
- if not d.query_exists(cancellable=None):
- logger.error("%s download folder does not exist: %s",
- download_folder_type, path)
- if show_error_in_log:
- severity = config.WARNING
- problem = _("%(file_type)s download folder does not exist") % {
- 'file_type': download_folder_type}
- details = _("Folder: %s") % path
- self.log_error(severity, problem, details)
- else:
- file_attributes = "standard::type,access::can-read,access::can-write"
- file_info = d.query_filesystem_info(file_attributes)
- file_type = file_info.get_file_type()
-
- if file_type != gio.FILE_TYPE_DIRECTORY and file_type != gio.FILE_TYPE_UNKNOWN:
- logger.error("%s download folder is invalid: %s",
- download_folder_type, path)
- if show_error_in_log:
- severity = config.WARNING
- problem = _("%(file_type)s download folder is invalid") % {
- 'file_type': download_folder_type}
- details = _("Folder: %s") % path
- self.log_error(severity, problem, details)
- else:
- # is the directory writable?
- try:
- temp_dir = tempfile.mkdtemp(prefix="rpd-tmp", dir=path)
- valid = True
- except:
- logger.error("%s is not writable", path)
- if show_error_in_log:
- severity = config.WARNING
- problem = _("%(file_type)s download folder is not writable") % {
- 'file_type': download_folder_type}
- details = _("Folder: %s") % path
- self.log_error(severity, problem, details)
- else:
- f = gio.File(temp_dir)
- f.delete(cancellable=None)
-
- except gio.Error, inst:
- logger.error("Error checking download directory %s", path)
- logger.error(inst)
-
- return valid
-
-
-
- # # #
- # Process results and management
- # # #
-
-
- def _start_process_managers(self):
- """
- Set up process managers.
-
- A task such as scanning a device or copying files is handled in its
- own process.
- """
-
- self.batch_size = 10
- self.batch_size_MB = 2
-
- sequence_values = (self.downloads_today_value,
- self.downloads_today_date_value,
- self.day_start_value,
- self.refresh_downloads_today_value,
- self.stored_sequence_value,
- self.uses_stored_sequence_no_value,
- self.uses_session_sequece_no_value,
- self.uses_sequence_letter_value)
-
- # daemon process to rename files and create subfolders
- self.subfolder_file_manager = SubfolderFileManager(
- self.subfolder_file_results,
- sequence_values)
-
- # process to scan source devices / paths
- self.scan_manager = ScanManager(self.scan_results, self.batch_size,
- self.device_collection.add_device)
-
- #process to copy files from source to destination
- self.copy_files_manager = CopyFilesManager(self.copy_files_results,
- self.batch_size_MB)
-
- #process to back files up
- self.backup_manager = BackupFilesManager(self.backup_results,
- self.batch_size_MB)
-
- #process to enhance files after they've been copied and before they're
- #renamed
- self.file_modify_manager = FileModifyManager(self.file_modify_results)
-
-
- def scan_results(self, source, condition):
- """
- Receive results from scan processes
- """
- connection = self.scan_manager.get_pipe(source)
-
- conn_type, data = connection.recv()
-
- if conn_type == rpdmp.CONN_COMPLETE:
- connection.close()
- self.scan_manager.no_tasks -= 1
- size, file_type_counter, scan_pid = data
- size = format_size_for_user(bytes=size)
- results_summary, file_types_present = file_type_counter.summarize_file_count()
- self.download_tracker.set_file_types_present(scan_pid, file_types_present)
- logger.info('Found %s' % results_summary)
- logger.info('Files total %s' % size)
- self.device_collection.update_device(scan_pid, size)
- self.device_collection.update_progress(scan_pid, 0.0, results_summary, 0, pulse=False)
- self.set_download_action_sensitivity()
-
- if (not self.auto_start_is_on and
- self.prefs.generate_thumbnails):
- self.download_progressbar.set_text(_("Thumbnails"))
- self.thumbnails.generate_thumbnails(scan_pid)
- elif self.auto_start_is_on:
- if self.need_job_code_for_naming and not self.job_code:
- self.get_job_code()
- else:
- self.start_download(scan_pid=scan_pid)
-
- logger.debug("Turning on display of thumbnails")
- self.display_scan_thumbnails()
- self.download_button.grab_focus()
-
- # signal that no more data is coming, finishing io watch for this pipe
- return False
- else:
- # partial results
- if len(data) > self.batch_size:
- logger.critical("incoming pipe length is unexpectedly long: %s" % len(data))
- else:
- size, file_type_counter, scan_pid, rpd_files = data
- size = format_size_for_user(bytes=size)
- scanning_progress = file_type_counter.running_file_count()
- self.device_collection.update_device(scan_pid, size)
- self.device_collection.update_progress(scan_pid, 0.0, scanning_progress, 0, pulse=True)
-
- for rpd_file in rpd_files:
- self.thumbnails.add_file(rpd_file=rpd_file,
- generate_thumbnail = not self.auto_start_is_on)
-
- # must return True for this method to be called again
- return True
-
-
- @dbus.service.method (config.DBUS_NAME,
- in_signature='', out_signature='b')
- def is_running (self):
- return self.running
-
- @dbus.service.method (config.DBUS_NAME,
- in_signature='', out_signature='')
- def start (self):
- if self.is_running():
- self.rapidapp.present()
- else:
- self.running = True
- gtk.main()
-
-def start():
-
- is_beta = config.version.find('~') > 0
-
- parser = OptionParser(version= "%%prog %s" % utilities.human_readable_version(config.version))
- parser.set_defaults(verbose=is_beta, extensions=False)
- # Translators: this text is displayed to the user when they request information on the command line options.
- # The text %default should not be modified or left out.
- parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help=_("display program information on the command line as the program runs (default: %default)"))
- parser.add_option("-d", "--debug", action="store_true", dest="debug", help=_('display debugging information when run from the command line'))
- parser.add_option("-q", "--quiet", action="store_false", dest="verbose", help=_("only output errors to the command line"))
- # image file extensions are recognized RAW files plus TIFF and JPG
- parser.add_option("-e", "--extensions", action="store_true", dest="extensions", help=_("list photo and video file extensions the program recognizes and exit"))
- parser.add_option("--focal-length", type=int, dest="focal_length", help="If an aperture value of 0.0 is encountered, the focal length metadata will be set to the number passed, and its aperture metadata to f/8")
- parser.add_option("-a", "--auto-detect", action="store_true", dest="auto_detect", help=_("automatically detect devices from which to download, overwriting existing program preferences"))
- parser.add_option("-l", "--device-location", type="string", metavar="PATH", dest="device_location", help=_("manually specify the PATH of the device from which to download, overwriting existing program preferences"))
- parser.add_option("--reset-settings", action="store_true", dest="reset", help=_("reset all program settings and preferences and exit"))
- (options, args) = parser.parse_args()
-
- if options.debug:
- logging_level = logging.DEBUG
- elif options.verbose:
- logging_level = logging.INFO
- else:
- logging_level = logging.ERROR
-
- logger.setLevel(logging_level)
-
- if options.auto_detect and options.device_location:
- logger.info(_("Error: specify device auto-detection or manually specify a device's path from which to download, but do not do both."))
- sys.exit(1)
-
- if options.auto_detect:
- auto_detect=True
- logger.info("Device auto detection set from command line")
- else:
- auto_detect=None
-
- if options.device_location:
- device_location=options.device_location
- if device_location[-1]=='/':
- device_location = device_location[:-1]
- logger.info("Device location set from command line: %s", device_location)
- else:
- device_location=None
-
- if options.extensions:
- extensions = ((rpdfile.PHOTO_EXTENSIONS, _("Photos:")), (rpdfile.VIDEO_EXTENSIONS, _("Videos:")))
- for exts, file_type in extensions:
- v = ''
- for e in exts[:-1]:
- v += '%s, ' % e.upper()
- v = file_type + " " + v[:-1] + ' '+ (_('and %s') % exts[-1].upper())
- print v
-
- sys.exit(0)
-
- if options.reset:
- prefs = prefsrapid.RapidPreferences()
- prefs.reset()
- print _("All settings and preferences have been reset")
- sys.exit(0)
-
- if options.focal_length:
- focal_length = options.focal_length
- else:
- focal_length = None
-
- logger.info("Rapid Photo Downloader %s", utilities.human_readable_version(config.version))
- logger.info("Using pyexiv2 %s", metadataphoto.pyexiv2_version_info())
- logger.info("Using exiv2 %s", metadataphoto.exiv2_version_info())
- if metadataexiftool.EXIFTOOL_VERSION is None:
- logger.info("Exiftool not detected")
- else:
- logger.info("Using exiftool %s", metadataexiftool.EXIFTOOL_VERSION)
- if metadatavideo.HAVE_HACHOIR:
- logger.info("Using hachoir %s", metadatavideo.version_info())
-
-
- if focal_length:
- logger.info("Focal length of %s will be used when an aperture of 0.0 is encountered", focal_length)
-
- bus = dbus.SessionBus ()
- request = bus.request_name (config.DBUS_NAME, dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
- if request != dbus.bus.REQUEST_NAME_REPLY_EXISTS:
- app = RapidApp(bus, '/', config.DBUS_NAME, focal_length=focal_length,
- auto_detect=auto_detect, device_location=device_location)
- else:
- # this application is already running
- print "Rapid Photo Downloader is already running"
- object = bus.get_object (config.DBUS_NAME, "/")
- app = dbus.Interface (object, config.DBUS_NAME)
-
- app.start()
-
-if __name__ == "__main__":
- start()