summaryrefslogtreecommitdiff
path: root/rapid
diff options
context:
space:
mode:
authorJulien Valroff <julien@kirya.net>2013-10-23 11:48:21 +0200
committerJulien Valroff <julien@kirya.net>2013-10-23 11:48:21 +0200
commite4926afd94329126adacce416fa8cf9daa32673b (patch)
tree69f107ed594fe3de037c59aeb870fa9ff77f23aa /rapid
parent063aca3a12dbf69d5ecdd2e949a788e01c91659d (diff)
Imported Upstream version 0.4.7upstream/0.4.7
Diffstat (limited to 'rapid')
-rw-r--r--rapid/ChangeLog29
-rw-r--r--rapid/backupfile.py29
-rw-r--r--rapid/config.py2
-rw-r--r--rapid/copyfiles.py107
-rw-r--r--rapid/generatename.py33
-rw-r--r--rapid/glade3/about.ui3
-rw-r--r--rapid/misc.py14
-rw-r--r--rapid/preferencesdialog.py28
-rwxr-xr-xrapid/rapid.py54
-rw-r--r--rapid/rpdfile.py153
-rwxr-xr-xrapid/scan.py128
-rw-r--r--rapid/subfolderfile.py91
12 files changed, 439 insertions, 232 deletions
diff --git a/rapid/ChangeLog b/rapid/ChangeLog
index cc27252..8269afb 100644
--- a/rapid/ChangeLog
+++ b/rapid/ChangeLog
@@ -1,3 +1,30 @@
+Version 0.4.7
+-------------
+
+2013-10-19
+
+Added feature to download audio files that are associated with photos such as
+those created by the Canon 1D series of cameras.
+
+Fixed bug #1242119: Choosing a new folder does not work in Ubuntu 13.10. In
+Ubuntu 13.10, choosing a destination or source folder from its bookmark does not
+work. The correct value is displayed in the file chooser button, but this value
+is not used by Rapid Photo Downloader.
+
+Fixed bug #1206853: Crashes when system message notifications not functioning
+properly.
+
+Fixed bug #909405: Allow selections by row (and not GTK default by square) when
+user is dragging the mouse or using the keyboard to select. Thank you to
+user 'Salukibob' for the patch.
+
+Added a KDE Solid action. Solid is KDE4's hardware-related framework. It detects
+when the user connects a new device and display a list of related actions.
+Thanks to dju` for the patch.
+
+Added Belarusian translation -- thanks go to Ilya Tsimokhin. Updated Swedish and
+Ukrainian translations.
+
Version 0.4.6
-------------
@@ -11,7 +38,7 @@ Added extra debugging output to help trace program execution progress.
Updated German and Spanish translations.
Version 0.4.6 Beta 1
--------------
+--------------------
2012-11-26
diff --git a/rapid/backupfile.py b/rapid/backupfile.py
index 310e665..7c91a19 100644
--- a/rapid/backupfile.py
+++ b/rapid/backupfile.py
@@ -54,7 +54,7 @@ class BackupFiles(multiprocessing.Process):
# As of Ubuntu 12.10 / Fedora 18, the file move/rename command is running agonisingly slowly
# A hackish workaround is to replace it with the standard python function
- self.use_gnome_file_operations = True
+ self.use_gnome_file_operations = False
def check_termination_request(self):
"""
@@ -99,12 +99,22 @@ class BackupFiles(multiprocessing.Process):
"""Backs up small files like XMP or THM files"""
source = gio.File(full_file_name)
dest_name = os.path.join(dest_dir, os.path.split(full_file_name)[1])
- logger.debug("Backing up %s", dest_name)
- dest=gio.File(dest_name)
- try:
- source.copy(dest, self.progress_callback_no_update, cancellable=None)
- except gio.Error, inst:
- logger.error("Failed to backup file %s", full_file_name)
+
+ if self.use_gnome_file_operations:
+ logger.debug("Backing up additional file %s...", dest_name)
+ dest=gio.File(dest_name)
+ try:
+ source.copy(dest, self.progress_callback_no_update, cancellable=None)
+ logger.debug("...backing up additional file %s succeeded", dest_name)
+ except gio.Error, inst:
+ logger.error("Failed to backup file %s: %s", full_file_name, inst)
+ else:
+ try:
+ logger.debug("Using python to back up additional file %s...", dest_name)
+ shutil.copy(full_file_name, dest_name)
+ logger.debug("...backing up additional file %s succeeded", dest_name)
+ except:
+ logger.error("Backup of %s failed", full_file_name)
def run(self):
@@ -209,10 +219,13 @@ class BackupFiles(multiprocessing.Process):
else:
rpd_file.status = config.STATUS_BACKUP_PROBLEM
else:
- # backup any THM or XMP files
+ # backup any THM, audio or XMP files
if rpd_file.download_thm_full_name:
self.backup_additional_file(dest_dir,
rpd_file.download_thm_full_name)
+ if rpd_file.download_audio_full_name:
+ self.backup_additional_file(dest_dir,
+ rpd_file.download_audio_full_name)
if rpd_file.download_xmp_full_name:
self.backup_additional_file(dest_dir,
rpd_file.download_xmp_full_name)
diff --git a/rapid/config.py b/rapid/config.py
index 977c342..1a12ea7 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.6'
+version = '0.4.7'
GCONF_KEY="/apps/rapid-photo-downloader"
diff --git a/rapid/copyfiles.py b/rapid/copyfiles.py
index 4a67ece..5bc243c 100644
--- a/rapid/copyfiles.py
+++ b/rapid/copyfiles.py
@@ -45,8 +45,8 @@ class CopyFiles(multiprocessing.Process):
"""
def __init__(self, photo_download_folder, video_download_folder, files,
modify_files_during_download, modify_pipe,
- scan_pid,
- batch_size_MB, results_pipe, terminate_queue,
+ scan_pid,
+ batch_size_MB, results_pipe, terminate_queue,
run_event):
multiprocessing.Process.__init__(self)
self.results_pipe = results_pipe
@@ -60,7 +60,7 @@ class CopyFiles(multiprocessing.Process):
self.scan_pid = scan_pid
self.no_files= len(self.files)
self.run_event = run_event
-
+
def check_termination_request(self):
"""
Check to see this process has not been requested to immediately terminate
@@ -71,8 +71,8 @@ class CopyFiles(multiprocessing.Process):
logger.info("Terminating file copying")
return True
return False
-
-
+
+
def update_progress(self, amount_downloaded, total):
# first check if process is being terminated
if not self.terminate_queue.empty():
@@ -86,65 +86,65 @@ class CopyFiles(multiprocessing.Process):
if amount_downloaded == total:
# this function is called a couple of times when total is reached
self.total_reached = True
-
+
self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_BYTES, (self.scan_pid, self.total_downloaded + amount_downloaded, chunk_downloaded))))
if amount_downloaded == total:
self.bytes_downloaded = 0
-
+
def progress_callback(self, amount_downloaded, total):
self.update_progress(amount_downloaded, total)
-
+
def thm_progress_callback(self, amount_downloaded, total):
# we don't care about tracking download progress for tiny THM files!
pass
-
+
def run(self):
"""start the actual copying of files"""
-
+
#characters used to generate temporary filenames
filename_characters = string.letters + string.digits
-
+
self.bytes_downloaded = 0
self.total_downloaded = 0
-
+
self.cancel_copy = gio.Cancellable()
-
+
self.create_temp_dirs()
-
+
# Send the location of both temporary directories, so they can be
# removed once another process attempts to rename all the files in them
# and move them to generated subfolders
- self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_TEMP_DIRS,
+ self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_TEMP_DIRS,
(self.scan_pid,
self.photo_temp_dir,
self.video_temp_dir))))
-
+
if self.photo_temp_dir or self.video_temp_dir:
-
+
self.thumbnail_maker = tn.Thumbnail()
-
+
for i in range(self.no_files):
rpd_file = self.files[i]
self.total_reached = False
-
+
# pause if instructed by the caller
self.run_event.wait()
-
+
if self.check_termination_request():
return None
-
+
source = gio.File(path=rpd_file.full_file_name)
-
+
#generate temporary name 5 digits long, no extension
temp_name = ''.join(random.choice(filename_characters) for i in xrange(5))
-
+
temp_full_file_name = os.path.join(
- self._get_dest_dir(rpd_file.file_type),
+ self._get_dest_dir(rpd_file.file_type),
temp_name)
rpd_file.temp_full_file_name = temp_full_file_name
dest = gio.File(path=temp_full_file_name)
-
+
copy_succeeded = False
try:
source.copy(dest, self.progress_callback, cancellable=self.cancel_copy)
@@ -154,24 +154,24 @@ class CopyFiles(multiprocessing.Process):
pn.DOWNLOAD_COPYING_ERROR_W_NO,
{'filetype': rpd_file.title})
rpd_file.add_extra_detail(
- pn.DOWNLOAD_COPYING_ERROR_W_NO_DETAIL,
+ pn.DOWNLOAD_COPYING_ERROR_W_NO_DETAIL,
{'errorno': inst.code, 'strerror': inst.message})
-
+
rpd_file.status = config.STATUS_DOWNLOAD_FAILED
-
+
rpd_file.error_title = rpd_file.problem.get_title()
rpd_file.error_msg = _("%(problem)s\nFile: %(file)s") % \
{'problem': rpd_file.problem.get_problems(),
'file': rpd_file.full_file_name}
-
+
logger.error("Failed to download file: %s", rpd_file.full_file_name)
logger.error(inst)
self.update_progress(rpd_file.size, rpd_file.size)
-
+
# increment this amount regardless of whether the copy actually
# succeeded or not. It's neccessary to keep the user informed.
self.total_downloaded += rpd_file.size
-
+
# copy THM (video thumbnail file) if there is one
if copy_succeeded and rpd_file.thm_full_name:
source = gio.File(path=rpd_file.thm_full_name)
@@ -186,8 +186,21 @@ class CopyFiles(multiprocessing.Process):
logger.error("Failed to download video THM file: %s", rpd_file.thm_full_name)
else:
temp_thm_full_name = None
-
-
+
+ #copy audio file if there is one
+ if copy_succeeded and rpd_file.audio_file_full_name:
+ source = gio.File(path=rpd_file.audio_file_full_name)
+ # reuse photo's file name
+ temp_audio_full_name = temp_full_file_name + '__rpd__audio'
+ dest = gio.File(path=temp_audio_full_name)
+ try:
+ source.copy(dest, self.thm_progress_callback, cancellable=self.cancel_copy)
+ rpd_file.temp_audio_full_name = temp_audio_full_name
+ logger.debug("Copied audio file %s", rpd_file.temp_audio_full_name)
+ except gio.Error, inst:
+ logger.error("Failed to download audio file: %s", rpd_file.audio_file_full_name)
+
+
if copy_succeeded and rpd_file.generate_thumbnail:
thumbnail, thumbnail_icon = self.thumbnail_maker.get_thumbnail(
temp_full_file_name,
@@ -197,33 +210,33 @@ class CopyFiles(multiprocessing.Process):
else:
thumbnail = None
thumbnail_icon = None
-
+
if rpd_file.metadata is not None:
rpd_file.metadata = None
-
-
+
+
download_count = i + 1
if self.modify_files_during_download and copy_succeeded:
copy_finished = download_count == self.no_files
-
- self.modify_pipe.send((rpd_file, download_count, temp_full_file_name,
+
+ self.modify_pipe.send((rpd_file, download_count, temp_full_file_name,
thumbnail_icon, thumbnail, copy_finished))
else:
- self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_FILE,
+ self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_FILE,
(copy_succeeded, rpd_file, download_count,
- temp_full_file_name,
+ temp_full_file_name,
thumbnail_icon, thumbnail))))
-
-
+
+
self.results_pipe.send((rpdmp.CONN_COMPLETE, None))
-
+
def _get_dest_dir(self, file_type):
if file_type == rpdfile.FILE_TYPE_PHOTO:
return self.photo_temp_dir
else:
return self.video_temp_dir
-
+
def _create_temp_dir(self, folder):
try:
temp_dir = tempfile.mkdtemp(prefix="rpd-tmp-", dir=folder)
@@ -234,15 +247,15 @@ class CopyFiles(multiprocessing.Process):
strerror,
folder)
temp_dir = None
-
+
return temp_dir
-
+
def create_temp_dirs(self):
self.photo_temp_dir = self.video_temp_dir = None
if self.photo_download_folder is not None:
self.photo_temp_dir = self._create_temp_dir(self.photo_download_folder)
if self.video_download_folder is not None:
self.video_temp_dir = self._create_temp_dir(self.photo_download_folder)
-
+
diff --git a/rapid/generatename.py b/rapid/generatename.py
index 6d138e8..446aaf8 100644
--- a/rapid/generatename.py
+++ b/rapid/generatename.py
@@ -113,19 +113,34 @@ class PhotoName:
logger.error("Both file modification time and metadata date & time are invalid for file %s", self.rpd_file.full_file_name)
return ''
- def _get_thm_extension(self):
+ def _get_associated_file_extension(self, associate_file):
"""
- Generates THM extension with correct capitalization, if needed
+ Generates extensions with correct capitalization for files like
+ thumbnail or audio files.
"""
- if self.rpd_file.thm_full_name:
- thm_extension = os.path.splitext(self.rpd_file.thm_full_name)[1]
+ if associate_file:
+ extension = os.path.splitext(associate_file)[1]
if self.L2 == UPPERCASE:
- thm_extension = thm_extension.upper()
+ extension = extension.upper()
elif self.L2 == LOWERCASE:
- thm_extension = thm_extension.lower()
- self.rpd_file.thm_extension = thm_extension
+ extension = extension.lower()
else:
- self.rpd_file.thm_extension = None
+ extension = None
+ return extension
+
+
+ def _get_thm_extension(self):
+ """
+ Generates THM extension with correct capitalization, if needed
+ """
+ self.rpd_file.thm_extension = self._get_associated_file_extension(self.rpd_file.thm_full_name)
+
+ def _get_audio_extension(self):
+ """
+ Generates audio extension with correct capitalization, if needed
+ e.g. WAV or wav
+ """
+ self.rpd_file.audio_extension = self._get_associated_file_extension(self.rpd_file.audio_file_full_name)
def _get_xmp_extension(self, extension):
"""
@@ -156,11 +171,13 @@ class PhotoName:
if self.L1 == NAME_EXTENSION:
filename = self.rpd_file.name
self._get_thm_extension()
+ self._get_audio_extension()
self._get_xmp_extension(extension)
elif self.L1 == NAME:
filename = name
elif self.L1 == EXTENSION:
self._get_thm_extension()
+ self._get_audio_extension()
self._get_xmp_extension(extension)
if extension:
if not self.strip_initial_period_from_extension:
diff --git a/rapid/glade3/about.ui b/rapid/glade3/about.ui
index 221c62e..0a82e74 100644
--- a/rapid/glade3/about.ui
+++ b/rapid/glade3/about.ui
@@ -34,6 +34,7 @@ Torben Gundtofte-Bruun &lt;torben@g-b.dk&gt;
Miroslav Matejaš &lt;silverspace@ubuntu-hr.org&gt;
Nicolás M. Zahlut &lt;nzahlut@live.com&gt;
Erik M
+Toni Lähdekorpi &lt;toni@lygon.net&gt;
Jose Luis Navarro &lt;jlnavarro111@gmail.com&gt;
Tomas Novak &lt;kuvaly@seznam.cz&gt;
Abel O'Rian &lt;abel.orian@gmail.com&gt;
@@ -47,7 +48,7 @@ Mikko Ruohola &lt;polarfox@polarfox.net&gt;
Ahmed Shubbar &lt;ahmed.shubbar@gmail.com&gt;
Sergei Sedov &lt;sedov@webmail.perm.ru&gt;
Marco Solari &lt;marcosolari@gmail.com&gt;
-Toni Lähdekorpi &lt;toni@lygon.net&gt;
+Ilya Tsimokhin &lt;ilya@tsimokhin.com&gt;
Ulf Urdén &lt;ulf.urden@purplescout.com&gt;
Julien Valroff &lt;julien@kirya.net&gt;
Aron Xu &lt;happyaron.xu@gmail.com&gt;
diff --git a/rapid/misc.py b/rapid/misc.py
index 69b4a93..a2c6d08 100644
--- a/rapid/misc.py
+++ b/rapid/misc.py
@@ -19,6 +19,8 @@
### USA
# modified by Damon Lynch 2009 to remove default bold formatting and alignment
+# modified by Damon Lynch 2103 to add function to get folder chosen by user in file chooser button
+
"""Module of commonly used helper classes and functions
"""
@@ -44,3 +46,15 @@ def run_dialog( text, secondarytext=None, parent=None, messagetype=gtk.MESSAGE_
ret = d.run()
d.destroy()
return ret
+
+def get_folder_selection(filechooserbutton):
+ """
+ Returns the path (folder) the user has chosen in a filechooserbutton
+ """
+ # this no longer works on Ubuntu 13.10:
+ # path = filechooserbutton.get_current_folder()
+ # but this works on Ubuntu 13.10:
+ path = filechooserbutton.get_filenames() #returns a list
+ if path:
+ path = path[0]
+ return path
diff --git a/rapid/preferencesdialog.py b/rapid/preferencesdialog.py
index 6e1f548..295b6e7 100644
--- a/rapid/preferencesdialog.py
+++ b/rapid/preferencesdialog.py
@@ -965,23 +965,33 @@ class PreferencesDialog():
self.treeview.set_cursor(0,column)
def on_download_folder_filechooser_button_selection_changed(self, widget):
- self.prefs.download_folder = widget.get_current_folder()
- self.update_photo_download_folder_example()
+ path = misc.get_folder_selection(widget)
+ if path:
+ self.prefs.download_folder = path
+ self.update_photo_download_folder_example()
def on_video_download_folder_filechooser_button_selection_changed(self, widget):
- self.prefs.video_download_folder = widget.get_current_folder()
- self.update_video_download_folder_example()
+ path = misc.get_folder_selection(widget)
+ if path:
+ self.prefs.video_download_folder = path
+ self.update_video_download_folder_example()
def on_backup_folder_filechooser_button_selection_changed(self, widget):
- self.prefs.backup_location = widget.get_current_folder()
- self.update_backup_example()
+ path = misc.get_folder_selection(widget)
+ if path:
+ self.prefs.backup_location = path
+ self.update_backup_example()
def on_backup_video_folder_filechooser_button_selection_changed(self, widget):
- self.prefs.backup_video_location = widget.get_current_folder()
- self.update_backup_example()
+ path = misc.get_folder_selection(widget)
+ if path:
+ self.prefs.backup_video_location = path
+ self.update_backup_example()
def on_device_location_filechooser_button_selection_changed(self, widget):
- self.prefs.device_location = widget.get_current_folder()
+ path = misc.get_folder_selection(widget)
+ if path:
+ self.prefs.device_location = path
def on_add_ignored_path_button_clicked(self, widget):
i = IgnorePathDialog(parent_window = self.dialog,
diff --git a/rapid/rapid.py b/rapid/rapid.py
index 7de18c2..4f7e6bc 100755
--- a/rapid/rapid.py
+++ b/rapid/rapid.py
@@ -54,6 +54,8 @@ logger = log_to_stderr()
import rpdfile
+from misc import get_folder_selection
+
import problemnotification as pn
import thumbnail as tn
import rpdmultiprocessing as rpdmp
@@ -343,7 +345,10 @@ class DeviceCollection(gtk.TreeView):
n = pynotify.Notification(title, message)
n.set_icon_from_pixbuf(self.parent_app.application_icon)
- n.show()
+ try:
+ n.show()
+ except:
+ logger.error("Unable to display message using notification system")
def create_cairo_image_surface(pil_image, image_width, image_height):
@@ -627,6 +632,20 @@ class ThumbnailDisplay(gtk.IconView):
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):
@@ -1838,7 +1857,7 @@ class RapidApp(dbus.service.Object):
if path:
if (path in self.prefs.device_blacklist and
self.search_for_PSD()):
- logger.info("%s ignored", mount.get_name())
+ 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)
@@ -2807,7 +2826,10 @@ class RapidApp(dbus.service.Object):
n = pynotify.Notification(notification_name, message)
n.set_icon_from_pixbuf(icon)
- n.show()
+ try:
+ n.show()
+ except:
+ logger.error("Unable to display message using notification system")
def notify_download_complete(self):
if self.display_summary_notification:
@@ -2859,7 +2881,10 @@ class RapidApp(dbus.service.Object):
if use_pynotify:
n = pynotify.Notification(PROGRAM_NAME, message)
n.set_icon_from_pixbuf(self.application_icon)
- n.show()
+ 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
@@ -3373,6 +3398,10 @@ class RapidApp(dbus.service.Object):
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()
@@ -3426,17 +3455,28 @@ class RapidApp(dbus.service.Object):
def on_from_filechooser_button_selection_changed(self, filechooserbutton):
logger.debug("on_from_filechooser_button_selection_changed")
- path = filechooserbutton.get_current_folder()
+ 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):
- path = filechooserbutton.get_current_folder()
+ 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 = filechooserbutton.get_current_folder()
+ path = get_folder_selection(filechooserbutton)
if path:
self.prefs.video_download_folder = path
diff --git a/rapid/rpdfile.py b/rapid/rpdfile.py
index a405782..3749554 100644
--- a/rapid/rpdfile.py
+++ b/rapid/rpdfile.py
@@ -42,20 +42,22 @@ import problemnotification as pn
import thumbnail as tn
-RAW_EXTENSIONS = ['arw', 'dcr', 'cr2', 'crw', 'dng', 'mos', 'mef', 'mrw',
- 'nef', 'nrw', 'orf', 'pef', 'raf', 'raw', 'rw2', 'sr2',
+RAW_EXTENSIONS = ['arw', 'dcr', 'cr2', 'crw', 'dng', 'mos', 'mef', 'mrw',
+ 'nef', 'nrw', 'orf', 'pef', 'raf', 'raw', 'rw2', 'sr2',
'srw']
-
+
JPEG_EXTENSIONS = ['jpg', 'jpe', 'jpeg']
NON_RAW_IMAGE_EXTENSIONS = JPEG_EXTENSIONS + ['tif', 'tiff']
PHOTO_EXTENSIONS = RAW_EXTENSIONS + NON_RAW_IMAGE_EXTENSIONS
+AUDIO_EXTENSIONS = ['wav', 'mp3']
+
if metadatavideo.DOWNLOAD_VIDEO:
- # some distros do not include the necessary libraries that Rapid Photo Downloader
+ # some distros do not include the necessary libraries that Rapid Photo Downloader
# needs to be able to download videos
- VIDEO_EXTENSIONS = ['3gp', 'avi', 'm2t', 'mov', 'mp4', 'mpeg','mpg', 'mod',
+ VIDEO_EXTENSIONS = ['3gp', 'avi', 'm2t', 'mov', 'mp4', 'mpeg','mpg', 'mod',
'tod']
if metadataexiftool.EXIFTOOL_VERSION is not None:
VIDEO_EXTENSIONS += ['mts']
@@ -71,7 +73,7 @@ FILE_TYPE_VIDEO = 1
def file_type(file_extension):
"""
Uses file extentsion to determine the type of file - photo or video.
-
+
Returns True if yes, else False.
"""
if file_extension in PHOTO_EXTENSIONS:
@@ -79,47 +81,50 @@ def file_type(file_extension):
elif file_extension in VIDEO_EXTENSIONS:
return FILE_TYPE_VIDEO
return None
-
-def get_rpdfile(extension, name, display_name, path, size,
- file_system_modification_time, thm_full_name,
+
+def get_rpdfile(extension, name, display_name, path, size,
+ file_system_modification_time,
+ thm_full_name, audio_file_full_name,
scan_pid, file_id, file_type):
-
+
if file_type == FILE_TYPE_VIDEO:
return Video(name, display_name, path, size,
file_system_modification_time, thm_full_name,
+ audio_file_full_name,
scan_pid, file_id)
else:
# assume it's a photo - no check for performance reasons (this will be
# called many times)
return Photo(name, display_name, path, size,
file_system_modification_time, thm_full_name,
+ audio_file_full_name,
scan_pid, file_id)
class FileTypeCounter:
def __init__(self):
self._counter = dict()
-
+
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
to show if a value refers to photos or videos or both, or just one
of each
"""
-
+
no_videos = self.no_videos()
no_images = self.no_photos()
-
+
if (no_videos > 0) and (no_images > 0):
v = _('photos and videos')
elif (no_videos == 0) and (no_images == 0):
@@ -134,14 +139,14 @@ class FileTypeCounter:
v = _('photos')
else:
v = _('photo')
- return v
-
+ return v
+
def count_files(self):
i = 0
for key in self._counter:
i += self._counter[key]
return i
-
+
def summarize_file_count(self):
"""
Summarizes the total number of photos and/or videos that can be
@@ -153,26 +158,27 @@ class FileTypeCounter:
file_types_present = self.file_types_present()
file_count_summary = _("%(number)s %(filetypes)s") % \
{'number':self.count_files(),
- 'filetypes': file_types_present}
+ '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.
+ 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
"""
- def __init__(self, name, display_name, path, size,
+ def __init__(self, name, display_name, path, size,
file_system_modification_time, thm_full_name,
+ audio_file_full_name,
scan_pid, file_id):
-
+
self.path = path
self.name = name
@@ -180,38 +186,43 @@ class RPDFile:
self.full_file_name = os.path.join(path, name)
self.extension = os.path.splitext(name)[1][1:].lower()
-
+
self.size = size # type int
-
+
self.modification_time = file_system_modification_time
-
+
#full path and name of thumbnail file that is associated with some videos
self.thm_full_name = thm_full_name
-
+
+ #full path and name of audio file that is associated with some photos and maybe one day videos
+ #think Canon 1D series of cameras
+ self.audio_file_full_name = audio_file_full_name
+
self.status = config.STATUS_NOT_DOWNLOADED
self.problem = None # class Problem in problemnotifcation.py
-
+
self._assign_file_type()
-
+
self.scan_pid = scan_pid
self.file_id = file_id
self.unique_id = str(scan_pid) + ":" + file_id
-
+
self.problem = None
self.job_code = None
-
+
# indicates whether to generate a thumbnail during the copy
# files process
self.generate_thumbnail = False
-
+
# generated values
-
+
self.temp_full_file_name = ''
self.temp_thm_full_name = ''
+ self.temp_audio_full_name = ''
self.temp_xmp_full_name = ''
-
+
self.download_start_time = None
-
+
self.download_subfolder = ''
self.download_path = ''
self.download_name = ''
@@ -219,63 +230,65 @@ class RPDFile:
self.download_full_base_name = '' #file name with path but no extension
self.download_thm_full_name = '' #name of THM (thumbnail) file with path
self.download_xmp_full_name = '' #name of XMP sidecar with path
-
+ self.download_audio_full_name = '' #name of the WAV or MP3 audio file with path
+
self.metadata = None
-
+
# Values that will be inserted in download process --
# (commented out because they're not needed until then)
-
+
#self.sequences = None
#self.download_folder
#self.subfolder_pref_list = []
#self.name_pref_list = []
#strip_characters = False
#self.thm_extension = ''
+ #self.wav_extension = ''
#self.xmp_extension = ''
-
- #these values are set only if they were written to an xmp sidecar
+
+ #these values are set only if they were written to an xmp sidecar
#in the filemodify process
#self.new_aperture = ''
#self.new_focal_length = ''
-
-
+
+
def _assign_file_type(self):
self.file_type = None
-
+
def _load_file_for_metadata(self, temp_file):
if temp_file:
return self.temp_full_file_name
else:
- return self.full_file_name
-
+ return self.full_file_name
+
def initialize_problem(self):
self.problem = pn.Problem()
# these next values are used to display in the error log window
# the information in them can vary from other forms of display of errors
self.error_title = self.error_msg = self.error_extra_detail = ''
-
+
def has_problem(self):
if self.problem is None:
return False
else:
return self.problem.has_problem()
-
+
def add_problem(self, component, problem_definition, *args):
if self.problem is None:
self.initialize_problem()
self.problem.add_problem(component, problem_definition, *args)
-
+
def add_extra_detail(self, extra_detail, *args):
self.problem.add_extra_detail(extra_detail, *args)
-
+
class Photo(RPDFile):
-
+
title = _("photo")
title_capitalized = _("Photo")
-
+
def _assign_file_type(self):
self.file_type = FILE_TYPE_PHOTO
-
+
def load_metadata(self, temp_file=False):
self.metadata = metadataphoto.MetaData(self._load_file_for_metadata(temp_file))
try:
@@ -285,16 +298,16 @@ class Photo(RPDFile):
return False
else:
return True
-
-
+
+
class Video(RPDFile):
-
+
title = _("video")
title_capitalized = _("Video")
-
+
def _assign_file_type(self):
self.file_type = FILE_TYPE_VIDEO
-
+
def load_metadata(self, temp_file=False):
if self.extension == 'mts' or not metadatavideo.HAVE_HACHOIR:
if metadatavideo.HAVE_HACHOIR:
@@ -303,31 +316,33 @@ class Video(RPDFile):
else:
self.metadata = metadatavideo.VideoMetaData(self._load_file_for_metadata(temp_file))
return True
-
+
class SamplePhoto(Photo):
def __init__(self, sample_name='IMG_0524.CR2', sequences=None):
Photo.__init__(self, name=sample_name,
display_name=sample_name,
path='/media/EOS_DIGITAL/DCIM/100EOS5D',
- size=23516764,
- file_system_modification_time=time.time(),
+ size=23516764,
+ file_system_modification_time=time.time(),
scan_pid=2033,
file_id='9873afe',
- thm_full_name=None)
+ thm_full_name=None,
+ audio_file_full_name=None)
self.sequences = sequences
self.metadata = metadataphoto.DummyMetaData()
self.download_start_time = datetime.datetime.now()
-
+
class SampleVideo(Video):
def __init__(self, sample_name='MVI_1379.MOV', sequences=None):
Video.__init__(self, name=sample_name,
display_name=sample_name,
path='/media/EOS_DIGITAL/DCIM/100EOS5D',
- size=823513764,
- file_system_modification_time=time.time(),
+ size=823513764,
+ file_system_modification_time=time.time(),
scan_pid=2033,
file_id='9873qrsfe',
- thm_full_name=None)
+ thm_full_name=None,
+ audio_file_full_name=None)
self.sequences = sequences
self.metadata = metadatavideo.DummyMetaData(filename=sample_name)
self.download_start_time = datetime.datetime.now()
diff --git a/rapid/scan.py b/rapid/scan.py
index 637031a..f0e4422 100755
--- a/rapid/scan.py
+++ b/rapid/scan.py
@@ -42,59 +42,71 @@ file_attributes = "standard::name,standard::display-name,\
standard::type,standard::size,time::modified,access::can-read,id::file"
-def get_video_THM_file(full_file_name_no_ext):
- """
- Checks to see if a thumbnail file (THM) is in the same directory as the
- file. Expects a full path to be part of the file name.
-
- Returns the filename, including path, if found, else returns None.
- """
-
+def get_associated_file(full_file_name_no_ext, extensions_to_check):
+
f = None
- for e in rpdfile.VIDEO_THUMBNAIL_EXTENSIONS:
+ for e in extensions_to_check:
if os.path.exists(full_file_name_no_ext + '.' + e):
f = full_file_name_no_ext + '.' + e
break
if os.path.exists(full_file_name_no_ext + '.' + e.upper()):
f = full_file_name_no_ext + '.' + e.upper()
break
-
- return f
+ return f
+
+def get_audio_file(full_file_name_no_ext):
+ """
+ Checks to see if an audio file of the same name is in the same directory
+ as the file. Expects a full path to be part of the file name.
+
+ Returns the filename, including path, if found, else returns None.
+ """
+ return get_associated_file(full_file_name_no_ext, rpdfile.AUDIO_EXTENSIONS)
+
+def get_video_THM_file(full_file_name_no_ext):
+ """
+ Checks to see if a thumbnail file (THM) is in the same directory as the
+ file. Expects a full path to be part of the file name.
+
+ Returns the filename, including path, if found, else returns None.
+ """
+
+ return get_associated_file(full_file_name_no_ext, rpdfile.VIDEO_THUMBNAIL_EXTENSIONS)
class Scan(multiprocessing.Process):
"""Scans the given path for files of a specified type.
-
+
Returns results in batches, finishing with a total of the size of all the
files in bytes.
"""
-
+
def __init__(self, path, ignored_paths, use_re_ignored_paths,
- batch_size, results_pipe,
+ batch_size, results_pipe,
terminate_queue, run_event):
-
+
"""Setup values needed to conduct the scan.
-
+
'path' is a string of the path to be scanned, which is passed to gio.
-
+
'ignored_paths' is a list of paths that should not be scanned. Any path
ending with one of the values will be ignored.
-
+
'use_re_ignored_paths': if true, pytho regular expressions will be used
to determine which paths to ignore
-
- 'batch_size' is the number of files that should be sent back to the
+
+ 'batch_size' is the number of files that should be sent back to the
calling function at one time.
-
+
'results_pipe' is a connection on which to send the results.
-
- 'terminate_queue' is a queue whose sole purpose is to notify the
+
+ 'terminate_queue' is a queue whose sole purpose is to notify the
process that it should terminate and not return any results.
-
+
'run_event' is an Event that is used to temporarily halt execution.
-
+
"""
-
+
multiprocessing.Process.__init__(self)
self.path = path
self.ignored_paths = ignored_paths
@@ -107,18 +119,18 @@ class Scan(multiprocessing.Process):
self.files_scanned = 0
self.files = []
self.file_type_counter = rpdfile.FileTypeCounter()
-
+
def _gio_scan(self, path, file_size_sum):
"""recursive function to scan a directory and its subdirectories
for photos and possibly videos"""
-
+
children = path.enumerate_children(file_attributes)
-
+
for child in children:
-
+
# pause if instructed by the caller
self.run_event.wait()
-
+
if not self.terminate_queue.empty():
x = self.terminate_queue.get()
# terminate immediately
@@ -128,26 +140,26 @@ class Scan(multiprocessing.Process):
# only collect files and scan in directories we can actually read
# cannot assume that users will download only from memory cards
-
+
if child.get_attribute_boolean(gio.FILE_ATTRIBUTE_ACCESS_CAN_READ):
file_type = child.get_file_type()
name = child.get_name()
if file_type == gio.FILE_TYPE_DIRECTORY:
if not self.ignore_this_path(name):
- file_size_sum = self._gio_scan(path.get_child(name),
+ file_size_sum = self._gio_scan(path.get_child(name),
file_size_sum)
if file_size_sum is None:
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:]
-
+
file_type = rpdfile.file_type(ext)
if file_type is not None:
file_id = child.get_attribute_string(
@@ -161,31 +173,35 @@ class Scan(multiprocessing.Process):
size = child.get_size()
modification_time = child.get_modification_time()
path_name = path.get_path()
-
+
# look for thumbnail file for videos
if file_type == rpdfile.FILE_TYPE_VIDEO:
thm_full_name = get_video_THM_file(os.path.join(path_name, base_name))
else:
thm_full_name = None
-
- scanned_file = rpdfile.get_rpdfile(ext,
- name,
- display_name,
+
+ # check if an audio file is associated with the photo or video
+ audio_file_full_name = get_audio_file(os.path.join(path_name, base_name))
+
+ scanned_file = rpdfile.get_rpdfile(ext,
+ name,
+ display_name,
path_name,
size,
modification_time,
- thm_full_name,
+ thm_full_name,
+ audio_file_full_name,
self.pid,
file_id,
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,
- (file_size_sum,
+ self.results_pipe.send((rpdmp.CONN_PARTIAL,
+ (file_size_sum,
self.file_type_counter,
self.pid,
self.files)))
@@ -193,14 +209,14 @@ class Scan(multiprocessing.Process):
self.counter = 0
return file_size_sum
-
+
def run(self):
"""start the actual scan."""
-
+
if self.use_re_ignored_paths and len(self.ignored_paths):
self.re_pattern = prefsrapid.check_and_compile_re(self.ignored_paths)
-
+
source = gio.File(self.path)
try:
if not self.ignore_this_path(self.path):
@@ -210,25 +226,25 @@ class Scan(multiprocessing.Process):
except gio.Error, inst:
logger.error("Error while scanning %s: %s", self.path, inst)
size = None
-
+
if size is not None:
if self.counter > 0:
# send any remaining results
- self.results_pipe.send((rpdmp.CONN_PARTIAL, (size,
+ 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.results_pipe.send((rpdmp.CONN_COMPLETE, (size,
self.file_type_counter, self.pid)))
- self.results_pipe.close()
+ self.results_pipe.close()
def ignore_this_path(self, path):
"""
determines if the path should be ignored according to the preferences
chosen by the user
"""
-
+
if len(self.ignored_paths):
if self.use_re_ignored_paths and self.re_pattern:
# regular expressions are being used
@@ -238,5 +254,5 @@ class Scan(multiprocessing.Process):
# regular expressions are not being used
if path.endswith(tuple(self.ignored_paths)):
return True
-
+
return False
diff --git a/rapid/subfolderfile.py b/rapid/subfolderfile.py
index 4e23309..ec99a6a 100644
--- a/rapid/subfolderfile.py
+++ b/rapid/subfolderfile.py
@@ -268,6 +268,67 @@ class SubfolderFile(multiprocessing.Process):
return rpd_file
+ def _move_associate_file(self, extension, full_base_name, temp_associate_file):
+ """Move (rename) the associate file using the pregenerated name
+
+ Returns tuple of result (True if succeeded, False otherwise) and
+ full path and filename"""
+
+ download_full_name = full_base_name + extension
+
+ # move (rename) associate file
+ if self.use_gnome_file_operations:
+ source = gio.File(path=temp_associate_file)
+ dest = gio.File(path=download_full_name)
+ try:
+ source.move(dest, self.progress_callback_no_update, cancellable=None)
+ success = True
+ except gio.Error, inst:
+ success = False
+ else:
+ try:
+ # don't check to see if it already exists
+ shutil.move(temp_associate_file, download_full_name)
+ success = True
+ except:
+ success = False
+
+ return (success, download_full_name)
+
+ def move_thm_file(self, rpd_file):
+ """Move (rename) the THM thumbnail file using the pregenerated name"""
+ ext = None
+ if hasattr(rpd_file, 'thm_extension'):
+ if rpd_file.thm_extension:
+ ext = rpd_file.thm_extension
+ if ext is None:
+ ext = '.THM'
+
+ result, rpd_file.download_thm_full_name = self._move_associate_file(ext, rpd_file.download_full_base_name, rpd_file.temp_thm_full_name)
+
+ if not result:
+ logger.error("Failed to move video THM file %s", rpd_file.download_thm_full_name)
+
+ return rpd_file
+
+ def move_audio_file(self, rpd_file):
+ """Move (rename) the associate audio file using the pregenerated name"""
+ ext = None
+ if hasattr(rpd_file, 'audio_extension'):
+ if rpd_file.audio_extension:
+ ext = rpd_file.audio_extension
+ if ext is None:
+ ext = '.WAV'
+
+ result, rpd_file.download_audio_full_name = self._move_associate_file(ext, rpd_file.download_full_base_name, rpd_file.temp_audio_full_name)
+
+ if not result:
+ logger.error("Failed to move file's associated audio file %s", rpd_file.download_audio_full_name)
+
+ return rpd_file
+
+
+
def run(self):
"""
Get subfolder and name.
@@ -487,7 +548,7 @@ class SubfolderFile(multiprocessing.Process):
else:
rpd_file = self.download_failure_file_error(rpd_file, inst.strerror)
except:
- rpd_file = self.download_failure_file_error(rpd_file, inst.strerror)
+ rpd_file = self.download_failure_file_error(rpd_file, "An unknown error occurred while renaming the file")
if add_unique_identifier:
name = os.path.splitext(rpd_file.download_name)
@@ -553,30 +614,10 @@ class SubfolderFile(multiprocessing.Process):
self.downloads_today_date.value = self.downloads_today_tracker.get_raw_downloads_today_date()
if rpd_file.temp_thm_full_name:
- ext = None
- if hasattr(rpd_file, 'thm_extension'):
- if rpd_file.thm_extension:
- ext = rpd_file.thm_extension
- if ext is None:
- ext = '.THM'
- download_thm_full_name = rpd_file.download_full_base_name + ext
-
- # copy and rename THM video file
- if self.use_gnome_file_operations:
- source = gio.File(path=rpd_file.temp_thm_full_name)
- dest = gio.File(path=download_thm_full_name)
- try:
- source.move(dest, self.progress_callback_no_update, cancellable=None)
- rpd_file.download_thm_full_name = download_thm_full_name
- except gio.Error, inst:
- logger.error("Failed to move video THM file %s", download_thm_full_name)
- else:
- try:
- # don't check to see if it already exists
- shutil.move(rpd_file.temp_thm_full_name, download_thm_full_name)
- rpd_file.download_thm_full_name = download_thm_full_name
- except:
- logger.error("Failed to move video THM file %s", download_thm_full_name)
+ rpd_file = self.move_thm_file(rpd_file)
+
+ if rpd_file.temp_audio_full_name:
+ rpd_file = self.move_audio_file(rpd_file)
if rpd_file.temp_xmp_full_name:
# copy and rename XMP sidecar file