diff options
Diffstat (limited to 'rapid')
-rw-r--r-- | rapid/ChangeLog | 37 | ||||
-rw-r--r-- | rapid/config.py | 2 | ||||
-rw-r--r-- | rapid/filemodify.py | 6 | ||||
-rwxr-xr-x | rapid/rapid.py | 157 | ||||
-rw-r--r-- | rapid/rpdfile.py | 25 | ||||
-rwxr-xr-x | rapid/scan.py | 22 |
6 files changed, 204 insertions, 45 deletions
diff --git a/rapid/ChangeLog b/rapid/ChangeLog index e2bd42f..6a28146 100644 --- a/rapid/ChangeLog +++ b/rapid/ChangeLog @@ -1,3 +1,40 @@ +Version 0.4.5 +------------- + +2012-06-24 + +Updated Dutch, Estonian, German, Italian, Norwegian and Polish translations. + +Updated man page. + + +Version 0.4.5 Beta 1 +-------------------- + +2012-06-17 + +To increase performance, thumbnails are now no longer displayed until all +devices have finished being scanned. To indicate the scan is occurring, the +progress bar now pulses and it displays a running total of the number of photos +and videos found. If scanning a very large number of files from a fast device, +the progress bar may pause. If this happens, just wait for the scan to complete. + +Fixed bug #1014203: Very poor program performance after download device changed. +The program now displays the results of scanning files much quicker if the +program's download device preferences are changed and a scan begins of a new +device. + +You can now specify via the command line whether you would like to automatically +detect devices from which to download, or manually specify the path of the +device. If specified, the option will overwrite the existing program +preferences. + +Added extra information to debugging output. + +Fixed bug #1014219: File Modify process crashes if program exits during +download. + + Version 0.4.4 ------------- diff --git a/rapid/config.py b/rapid/config.py index 64228e8..d44c9bb 100644 --- a/rapid/config.py +++ b/rapid/config.py @@ -16,7 +16,7 @@ ### Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 ### USA -version = '0.4.4' +version = '0.4.5' GCONF_KEY="/apps/rapid-photo-downloader" diff --git a/rapid/filemodify.py b/rapid/filemodify.py index 8e5d8a3..98921ae 100644 --- a/rapid/filemodify.py +++ b/rapid/filemodify.py @@ -72,7 +72,11 @@ class FileModify(multiprocessing.Process): while not copy_finished: logger.debug("Finished %s. Getting next task.", download_count) - rpd_file, download_count, temp_full_file_name, thumbnail_icon, thumbnail, copy_finished = self.results_pipe.recv() + data = self.results_pipe.recv() + if len(data) > 2: + rpd_file, download_count, temp_full_file_name, thumbnail_icon, thumbnail, copy_finished = data + else: + rpd_file, copy_finished = data if rpd_file is None: # this is a termination signal logger.info("Terminating file modify via pipe") diff --git a/rapid/rapid.py b/rapid/rapid.py index a060db3..fab5796 100755 --- a/rapid/rapid.py +++ b/rapid/rapid.py @@ -32,6 +32,8 @@ 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 @@ -135,9 +137,9 @@ class DeviceCollection(gtk.TreeView): 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 + # process id, pulse self.liststore = gtk.ListStore(gtk.gdk.Pixbuf, str, str, float, str, - gtk.gdk.Pixbuf, int) + gtk.gdk.Pixbuf, int, int) self.map_process_to_row = {} self.devices_by_scan_pid = {} @@ -176,7 +178,8 @@ class DeviceCollection(gtk.TreeView): column2 = gtk.TreeViewColumn(_("Download Progress"), gtk.CellRendererProgress(), value=3, - text=4) + text=4, + pulse=7) self.append_column(column2) self.show_all() @@ -211,7 +214,8 @@ class DeviceCollection(gtk.TreeView): progress, progress_bar_text, eject, - process_id)) + process_id, + -1)) self._set_process_map(process_id, iter) @@ -275,7 +279,7 @@ class DeviceCollection(gtk.TreeView): else: return None - def update_progress(self, scan_pid, percent_complete, progress_bar_text, bytes_downloaded): + def update_progress(self, scan_pid, percent_complete, progress_bar_text, bytes_downloaded, pulse=None): iter = self._get_process_map(scan_pid) if iter: @@ -283,9 +287,19 @@ class DeviceCollection(gtk.TreeView): self.liststore.set_value(iter, 3, percent_complete) if progress_bar_text: self.liststore.set_value(iter, 4, progress_bar_text) - if percent_complete or bytes_downloaded: - pass - #~ logger.info("Implement update overall progress") + + 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): """ @@ -502,21 +516,9 @@ class ThumbnailDisplay(gtk.IconView): self.DOWNLOAD_STATUS_COL = 7 self.STATUS_ICON_COL = 8 - 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 - ) + self._create_liststore() self.clear() - self.set_model(self.liststore) - checkbutton = gtk.CellRendererToggle() checkbutton.set_radio(False) @@ -546,6 +548,22 @@ class ThumbnailDisplay(gtk.IconView): 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 @@ -1018,7 +1036,13 @@ class ThumbnailDisplay(gtk.IconView): have been downloaded. """ if scan_pid is None and not keep_downloaded_files: - self.liststore.clear() + + # 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 = {} @@ -1036,6 +1060,9 @@ class ThumbnailDisplay(gtk.IconView): 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): @@ -1503,7 +1530,8 @@ class RapidApp(dbus.service.Object): processes. """ - def __init__(self, bus, path, name, taskserver=None, focal_length=None): + 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 @@ -1513,7 +1541,7 @@ class RapidApp(dbus.service.Object): self.focal_length = focal_length # Setup program preferences, and set callback for when they change - self._init_prefs() + self._init_prefs(auto_detect, device_location) # Initialize widgets in the main window, and variables that point to them self._init_widgets() @@ -1696,12 +1724,14 @@ class RapidApp(dbus.service.Object): if self.preview_image.unique_id is not None: self.thumbnails.show_prev_image(self.preview_image.unique_id) - def set_thumbnail_sort(self): + def display_scan_thumbnails(self): """ - If all the scans are complete, sets the sort order + 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() # # # @@ -1740,7 +1770,7 @@ class RapidApp(dbus.service.Object): user said no. """ l = self.prefs.device_location - if l in ['/media', os.path.expanduser('~'), '/']: + 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 @@ -2403,7 +2433,19 @@ class RapidApp(dbus.service.Object): 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) + 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, @@ -2881,8 +2923,17 @@ class RapidApp(dbus.service.Object): # # # - def _init_prefs(self): + 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 @@ -3006,7 +3057,7 @@ class RapidApp(dbus.service.Object): def post_preference_change(self): if self.rerun_setup_available_image_and_video_media: - logger.info("Download device settings preferences were changed.") + 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) @@ -3658,12 +3709,19 @@ class RapidApp(dbus.service.Object): 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 need_video_folder: if not self.is_valid_download_dir(self.prefs.video_download_folder, is_photo_dir=False): valid = False invalid_dirs.append(self.prefs.video_download_folder) + else: + logger.debug("Video download folder is valid: %s", + self.prefs.video_download_folder) + return (valid, invalid_dirs) @@ -3823,7 +3881,7 @@ class RapidApp(dbus.service.Object): logger.info('Found %s' % results_summary) logger.info('Files total %s' % size) self.device_collection.update_device(scan_pid, size) - self.device_collection.update_progress(scan_pid, 0.0, results_summary, 0) + self.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 @@ -3836,16 +3894,24 @@ class RapidApp(dbus.service.Object): else: self.start_download(scan_pid=scan_pid) - self.set_thumbnail_sort() + 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: - for rpd_file in data: + 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) @@ -3880,7 +3946,9 @@ def start(): 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 f8") + 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() @@ -3892,6 +3960,24 @@ def start(): 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:"))) @@ -3932,7 +4018,8 @@ def start(): 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) + 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" diff --git a/rapid/rpdfile.py b/rapid/rpdfile.py index 99f6aa5..cf38ebc 100644 --- a/rapid/rpdfile.py +++ b/rapid/rpdfile.py @@ -67,7 +67,6 @@ else: FILE_TYPE_PHOTO = 0 FILE_TYPE_VIDEO = 1 - def file_type(file_extension): """ Uses file extentsion to determine the type of file - photo or video. @@ -102,6 +101,14 @@ class FileTypeCounter: def add(self, file_type): self._counter[file_type] = self._counter.setdefault(file_type, 0) + 1 + def no_videos(self): + """Returns the number of videos""" + return self._counter.setdefault(FILE_TYPE_VIDEO, 0) + + def no_photos(self): + """Returns the number of photos""" + return self._counter.setdefault(FILE_TYPE_PHOTO, 0) + def file_types_present(self): """ returns a string to be displayed to the user that can be used @@ -109,8 +116,8 @@ class FileTypeCounter: of each """ - no_videos = self._counter.setdefault(FILE_TYPE_VIDEO, 0) - no_images = self._counter.setdefault(FILE_TYPE_PHOTO, 0) + no_videos = self.no_videos() + no_images = self.no_photos() if (no_videos > 0) and (no_images > 0): v = _('photos and videos') @@ -135,6 +142,10 @@ class FileTypeCounter: return i def summarize_file_count(self): + """ + Summarizes the total number of photos and/or videos that can be + downloaded. Displayed after a scan is finished. + """ #Number of files, e.g. "433 photos and videos" or "23 videos". #Displayed in the progress bar at the top of the main application #window. @@ -144,6 +155,14 @@ class FileTypeCounter: 'filetypes': file_types_present} return (file_count_summary, file_types_present) + def running_file_count(self): + """ + Displays raw numbers of photos and videos. Displayed as a scan is + occurring. + """ + return _("scanning (found %(photos)s photos and %(videos)s videos)...") % ({'photos': self.no_photos(), + 'videos': self.no_videos()}) + class RPDFile: """ Base class for photo or video file, with metadata diff --git a/rapid/scan.py b/rapid/scan.py index c6ccaf8..637031a 100755 --- a/rapid/scan.py +++ b/rapid/scan.py @@ -104,6 +104,7 @@ class Scan(multiprocessing.Process): self.run_event = run_event self.batch_size = batch_size self.counter = 0 + self.files_scanned = 0 self.files = [] self.file_type_counter = rpdfile.FileTypeCounter() @@ -139,6 +140,11 @@ class Scan(multiprocessing.Process): return None elif file_type == gio.FILE_TYPE_REGULAR: + + self.files_scanned += 1 + if self.files_scanned % 100 == 0: + logger.debug("Scanned %s files", self.files_scanned) + base_name, ext = os.path.splitext(name) ext = ext.lower()[1:] @@ -174,15 +180,17 @@ class Scan(multiprocessing.Process): file_type) self.files.append(scanned_file) - + file_size_sum += size + if self.counter == self.batch_size: # send batch of results self.results_pipe.send((rpdmp.CONN_PARTIAL, - self.files)) + (file_size_sum, + self.file_type_counter, + self.pid, + self.files))) self.files = [] self.counter = 0 - - file_size_sum += size return file_size_sum @@ -206,7 +214,11 @@ class Scan(multiprocessing.Process): if size is not None: if self.counter > 0: # send any remaining results - self.results_pipe.send((rpdmp.CONN_PARTIAL, self.files)) + self.results_pipe.send((rpdmp.CONN_PARTIAL, (size, + self.file_type_counter, + self.pid, + self.files))) + self.results_pipe.send((rpdmp.CONN_COMPLETE, (size, self.file_type_counter, self.pid))) self.results_pipe.close() |