summaryrefslogtreecommitdiff
path: root/rapid
diff options
context:
space:
mode:
Diffstat (limited to 'rapid')
-rw-r--r--rapid/ChangeLog37
-rw-r--r--rapid/backupfile.py111
-rw-r--r--rapid/config.py6
-rw-r--r--rapid/generatename.py156
-rw-r--r--rapid/glade3/about.ui3
-rw-r--r--rapid/glade3/xmp.ui1376
-rwxr-xr-xrapid/rapid.py1771
-rw-r--r--rapid/rpdfile.py3
-rw-r--r--rapid/subfolderfile.py408
9 files changed, 2723 insertions, 1148 deletions
diff --git a/rapid/ChangeLog b/rapid/ChangeLog
index 6a28146..cc27252 100644
--- a/rapid/ChangeLog
+++ b/rapid/ChangeLog
@@ -1,3 +1,38 @@
+Version 0.4.6
+-------------
+
+2013-01-22
+
+Fixed bug #1083756: Application shows duplicate sources.
+Fixed bug #1093330: Photo rename ignores SubSeconds when 00.
+
+Added extra debugging output to help trace program execution progress.
+
+Updated German and Spanish translations.
+
+Version 0.4.6 Beta 1
+-------------
+
+2012-11-26
+
+Fixed bug #1023586: Added RAW file support for Nikon NRW files. Rapid Photo
+Downloader uses the exiv2 program to read a photo's metadata. Although the NRW
+format is not officially supported by exiv2, it appears to work. If you have
+NRW files and Rapid Photo Downloader crashes while reading this files, please
+file a bug report.
+
+Preliminary and tentative fix for bug #1025908: Application freezes under
+Ubuntu 12.10. This fix should not be considered final, and needs further
+testing.
+
+Added Arabic translation. Updated Czech, Danish, French, Italian, Norwegian,
+Russian, Serbian, Spanish and Swedish translations.
+
+Fixed missing dependencies on python-dbus and exiv2 in Debian/control file.
+
+Added extra debugging output to help trace program execution progress.
+
+
Version 0.4.5
-------------
@@ -113,6 +148,8 @@ confirm if this is really what they want to do.
Fixed bug #792228: clear all thumbnails when refresh command issued.
+Fixed bug #890949: Panasonic MOD format and duplicate filename issue
+
Fixed a bug where the device progress bar would occasionally disappear when
the download device was changed.
diff --git a/rapid/backupfile.py b/rapid/backupfile.py
index 6581e6d..310e665 100644
--- a/rapid/backupfile.py
+++ b/rapid/backupfile.py
@@ -23,6 +23,7 @@ import tempfile
import os
import gio
+import shutil
import logging
logger = multiprocessing.get_logger()
@@ -41,7 +42,7 @@ from gettext import gettext as _
class BackupFiles(multiprocessing.Process):
def __init__(self, path, name,
- batch_size_MB, results_pipe, terminate_queue,
+ batch_size_MB, results_pipe, terminate_queue,
run_event):
multiprocessing.Process.__init__(self)
self.results_pipe = results_pipe
@@ -50,7 +51,11 @@ class BackupFiles(multiprocessing.Process):
self.path = path
self.mount_name = name
self.run_event = run_event
-
+
+ # 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
+
def check_termination_request(self):
"""
Check to see this process has not been requested to immediately terminate
@@ -61,8 +66,8 @@ class BackupFiles(multiprocessing.Process):
logger.info("Terminating file backup")
return True
return False
-
-
+
+
def update_progress(self, amount_downloaded, total):
# first check if process is being terminated
self.amount_downloaded = amount_downloaded
@@ -74,22 +79,22 @@ class BackupFiles(multiprocessing.Process):
chunk_downloaded = amount_downloaded - self.bytes_downloaded
if (chunk_downloaded > self.batch_size_bytes) or (amount_downloaded == total):
self.bytes_downloaded = amount_downloaded
-
+
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.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 progress_callback_no_update(self, amount_downloaded, total):
"""called when copying very small files"""
pass
-
+
def backup_additional_file(self, dest_dir, full_file_name):
"""Backs up small files like XMP or THM files"""
source = gio.File(full_file_name)
@@ -99,55 +104,57 @@ class BackupFiles(multiprocessing.Process):
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)
-
+ logger.error("Failed to backup file %s", full_file_name)
+
def run(self):
-
+
self.cancel_copy = gio.Cancellable()
self.bytes_downloaded = 0
self.total_downloaded = 0
-
+
while True:
-
+
self.amount_downloaded = 0
- move_succeeded, rpd_file, path_suffix, backup_duplicate_overwrite = self.results_pipe.recv()
+ move_succeeded, rpd_file, path_suffix, backup_duplicate_overwrite, download_count = self.results_pipe.recv()
if rpd_file is None:
# this is a termination signal
return None
# pause if instructed by the caller
self.run_event.wait()
-
+
if self.check_termination_request():
return None
-
+
backup_succeeded = False
self.scan_pid = rpd_file.scan_pid
-
+
if move_succeeded:
self.total_reached = False
-
+
source = gio.File(path=rpd_file.download_full_file_name)
-
+
if path_suffix is None:
dest_base_dir = self.path
else:
dest_base_dir = os.path.join(self.path, path_suffix)
-
-
+
+
dest_dir = os.path.join(dest_base_dir, rpd_file.download_subfolder)
backup_full_file_name = os.path.join(
- dest_dir,
- rpd_file.download_name)
-
+ dest_dir,
+ rpd_file.download_name)
+
subfolder = gio.File(path=dest_dir)
if not subfolder.query_exists(cancellable=None):
# create the subfolders on the backup path
try:
+ logger.debug("Creating subfolder %s on backup device %s...", dest_dir, self.mount_name)
subfolder.make_directory_with_parents(cancellable=gio.Cancellable())
+ logger.debug("...backup subfolder created")
except gio.Error, inst:
# There is a tiny chance directory may have been created by
# another process between the time it takes to query and
- # the time it takes to create a new directory.
+ # the time it takes to create a new directory.
# Ignore such errors.
if inst.code <> gio.ERROR_EXISTS:
logger.error("Failed to create backup subfolder: %s", dest_dir)
@@ -159,7 +166,7 @@ class BackupFiles(multiprocessing.Process):
_("Destination directory could not be created: %(directory)s\n") % \
{'directory': subfolder, } + \
_("Source: %(source)s\nDestination: %(destination)s") % \
- {'source': rpd_file.download_full_file_name,
+ {'source': rpd_file.download_full_file_name,
'destination': backup_full_file_name} + "\n" + \
_("Error: %(inst)s") % {'inst': inst}
@@ -168,21 +175,33 @@ class BackupFiles(multiprocessing.Process):
flags = gio.FILE_COPY_OVERWRITE
else:
flags = gio.FILE_COPY_NONE
-
- try:
- source.copy(dest, self.progress_callback, flags,
- cancellable=self.cancel_copy)
- backup_succeeded = True
- except gio.Error, inst:
- fileNotBackedUpMessageDisplayed = True
- rpd_file.add_problem(None, pn.BACKUP_ERROR, self.mount_name)
- rpd_file.add_extra_detail('%s%s' % (pn.BACKUP_ERROR, self.mount_name), inst)
- rpd_file.error_title = _('Backing up error')
- rpd_file.error_msg = \
- _("Source: %(source)s\nDestination: %(destination)s") % \
- {'source': rpd_file.download_full_file_name, 'destination': backup_full_file_name} + "\n" + \
- _("Error: %(inst)s") % {'inst': inst}
- logger.error("%s:\n%s", rpd_file.error_title, rpd_file.error_msg)
+
+ if self.use_gnome_file_operations:
+ try:
+ logger.debug("Backing up file %s on device %s...", download_count, self.mount_name)
+ source.copy(dest, self.progress_callback, flags,
+ cancellable=self.cancel_copy)
+ backup_succeeded = True
+ logger.debug("...backing up file %s on device %s succeeded", download_count, self.mount_name)
+ except gio.Error, inst:
+ fileNotBackedUpMessageDisplayed = True
+ rpd_file.add_problem(None, pn.BACKUP_ERROR, self.mount_name)
+ rpd_file.add_extra_detail('%s%s' % (pn.BACKUP_ERROR, self.mount_name), inst)
+ rpd_file.error_title = _('Backing up error')
+ rpd_file.error_msg = \
+ _("Source: %(source)s\nDestination: %(destination)s") % \
+ {'source': rpd_file.download_full_file_name, 'destination': backup_full_file_name} + "\n" + \
+ _("Error: %(inst)s") % {'inst': inst}
+ logger.error("%s:\n%s", rpd_file.error_title, rpd_file.error_msg)
+ else:
+ try:
+ logger.debug("Using python to back up file %s on device %s...", download_count, self.mount_name)
+ shutil.copy(rpd_file.download_full_file_name, backup_full_file_name)
+ backup_succeeded = True
+ logger.debug("...backing up file %s on device %s succeeded", download_count, self.mount_name)
+ except:
+ logger.error("Backup of %s failed", backup_full_file_name)
+
if not backup_succeeded:
if rpd_file.status == config.STATUS_DOWNLOAD_FAILED:
@@ -197,17 +216,17 @@ class BackupFiles(multiprocessing.Process):
if rpd_file.download_xmp_full_name:
self.backup_additional_file(dest_dir,
rpd_file.download_xmp_full_name)
-
+
self.total_downloaded += rpd_file.size
bytes_not_downloaded = rpd_file.size - self.amount_downloaded
if bytes_not_downloaded:
self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_BYTES, (self.scan_pid, self.pid, self.total_downloaded, bytes_not_downloaded))))
-
+
self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_FILE,
(backup_succeeded, rpd_file))))
-
-
+
+
diff --git a/rapid/config.py b/rapid/config.py
index d44c9bb..977c342 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.5'
+version = '0.4.6'
GCONF_KEY="/apps/rapid-photo-downloader"
@@ -30,9 +30,9 @@ MEDIA_LOCATION = "/media"
SKIP_DOWNLOAD = "skip download"
ADD_UNIQUE_IDENTIFIER = "add unique identifier"
-# These next three values are fall back values that are used only
+# These next three values are fall back values that are used only
# if calls to xdg-user-dir fail
-DEFAULT_PHOTO_LOCATIONS = ['Pictures', 'Photos']
+DEFAULT_PHOTO_LOCATIONS = ['Pictures', 'Photos']
DEFAULT_BACKUP_LOCATION = 'Pictures'
DEFAULT_VIDEO_BACKUP_LOCATION = 'Videos'
diff --git a/rapid/generatename.py b/rapid/generatename.py
index 84c9310..6d138e8 100644
--- a/rapid/generatename.py
+++ b/rapid/generatename.py
@@ -43,18 +43,18 @@ class PhotoName:
Generate the name of a photo. Used as a base class for generating names
of videos, as well as subfolder names for both file types
"""
-
+
def __init__(self, pref_list):
self.pref_list = pref_list
-
+
# Some of the next values are overwritten in derived classes
self.strip_initial_period_from_extension = False
self.strip_forward_slash = True
self.L1_date_check = IMAGE_DATE #used in _get_date_component()
self.component = pn.FILENAME_COMPONENT #used in error reporting
-
-
+
+
def _get_values_from_pref_list(self):
for i in range(0, len(self.pref_list), 3):
yield (self.pref_list[i], self.pref_list[i+1], self.pref_list[i+2])
@@ -64,19 +64,19 @@ class PhotoName:
Returns portion of new file / subfolder name based on date time.
If the date is missing, will attempt to use the fallback date.
"""
-
+
# step 1: get the correct value from metadata
if self.L1 == self.L1_date_check:
if self.L2 == SUBSECONDS:
- d = self.rpd_file.metadata.sub_seconds()
- if d == '00':
+ d = self.rpd_file.metadata.sub_seconds(missing=None)
+ if d is None:
self.rpd_file.problem.add_problem(self.component, pn.MISSING_METADATA, _(self.L2))
return ''
else:
return d
else:
d = self.rpd_file.metadata.date_time(missing=None)
-
+
elif self.L1 == TODAY:
d = datetime.datetime.now()
elif self.L1 == YESTERDAY:
@@ -86,7 +86,7 @@ class PhotoName:
d = self.rpd_file.download_start_time
else:
raise("Date options invalid")
-
+
# step 2: if have a value, try to convert it to string format
if d:
try:
@@ -105,14 +105,14 @@ class PhotoName:
else:
self.rpd_file.add_problem(self.component, pn.MISSING_METADATA, _(self.L1))
return ''
-
+
try:
return d.strftime(convert_date_for_strftime(self.L2))
except:
self.rpd_file.add_problem(self.component, pn.INVALID_DATE_TIME, d)
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):
"""
Generates THM extension with correct capitalization, if needed
@@ -126,7 +126,7 @@ class PhotoName:
self.rpd_file.thm_extension = thm_extension
else:
self.rpd_file.thm_extension = None
-
+
def _get_xmp_extension(self, extension):
"""
Generates XMP extension with correct capitalization, if needed.
@@ -144,15 +144,15 @@ class PhotoName:
self.rpd_file.xmp_extension = '.xmp'
else:
self.rpd_file.xmp_extension = None
-
-
+
+
def _get_filename_component(self):
"""
Returns portion of new file / subfolder name based on the file name
"""
-
+
name, extension = os.path.splitext(self.rpd_file.name)
-
+
if self.L1 == NAME_EXTENSION:
filename = self.rpd_file.name
self._get_thm_extension()
@@ -178,10 +178,10 @@ class PhotoName:
n = re.search("(?P<image_number>[0-9]+$)", name)
if not n:
self.rpd_file.add_problem(self.component, pn.MISSING_IMAGE_NUMBER)
- return ''
+ return ''
else:
image_number = n.group("image_number")
-
+
if self.L2 == IMAGE_NUMBER_ALL:
filename = image_number
elif self.L2 == IMAGE_NUMBER_1:
@@ -201,14 +201,14 @@ class PhotoName:
filename = filename.lower()
return filename
-
+
def _get_metadata_component(self):
"""
Returns portion of new image / subfolder name based on the metadata
-
+
Note: date time metadata found in _getDateComponent()
"""
-
+
if self.L1 == APERTURE:
v = self.rpd_file.metadata.aperture()
elif self.L1 == ISO:
@@ -247,7 +247,7 @@ class PhotoName:
else:
raise TypeError("Invalid metadata option specified")
if self.L1 in [CAMERA_MAKE, CAMERA_MODEL, SHORT_CAMERA_MODEL,
- SHORT_CAMERA_MODEL_HYPHEN, OWNER_NAME, ARTIST,
+ SHORT_CAMERA_MODEL_HYPHEN, OWNER_NAME, ARTIST,
COPYRIGHT]:
if self.L2 == UPPERCASE:
v = v.upper()
@@ -269,49 +269,49 @@ class PhotoName:
x= x / 26 - 1
v = string.lowercase[r] + v
v = string.lowercase[x] + v
-
+
return v
-
-
+
+
v = _letters(sequence)
if self.L2 == UPPERCASE:
v = v.upper()
-
+
return v
def _format_sequence_no(self, value, amountToPad):
padding = LIST_SEQUENCE_NUMBERS_L2.index(amountToPad) + 1
formatter = '%0' + str(padding) + "i"
return formatter % value
-
+
def _get_downloads_today(self):
return self._format_sequence_no(self.rpd_file.sequences.get_downloads_today(), self.L2)
def _get_session_sequence_no(self):
- return self._format_sequence_no(self.rpd_file.sequences.get_session_sequence_no(), self.L2)
-
+ return self._format_sequence_no(self.rpd_file.sequences.get_session_sequence_no(), self.L2)
+
def _get_stored_sequence_no(self):
return self._format_sequence_no(self.rpd_file.sequences.get_stored_sequence_no(), self.L2)
def _get_sequence_letter(self):
return self._calculate_letter_sequence(self.rpd_file.sequences.get_sequence_letter())
-
+
def _get_sequences_component(self):
if self.L1 == DOWNLOAD_SEQ_NUMBER:
- return self._get_downloads_today()
+ return self._get_downloads_today()
elif self.L1 == SESSION_SEQ_NUMBER:
return self._get_session_sequence_no()
elif self.L1 == STORED_SEQ_NUMBER:
- return self._get_stored_sequence_no()
+ return self._get_stored_sequence_no()
elif self.L1 == SEQUENCE_LETTER:
- return self._get_sequence_letter()
+ return self._get_sequence_letter()
#~ elif self.L1 == SUBFOLDER_SEQ_NUMBER:
#~ return self._getSubfolderSequenceNo()
-
-
-
+
+
+
def _get_component(self):
try:
if self.L0 == DATE_TIME:
@@ -331,8 +331,8 @@ class PhotoName:
except:
self.rpd_file.add_problem(self.component, pn.ERROR_IN_GENERATION, _(self.L0))
return ''
-
-
+
+
def generate_name(self, rpd_file):
self.rpd_file = rpd_file
@@ -342,89 +342,89 @@ class PhotoName:
v = self._get_component()
if v:
name += v
-
+
# remove any null characters - they are bad news in filenames
name = name.replace('\x00', '')
if self.rpd_file.strip_characters:
for c in r'\:*?"<>|':
name = name.replace(c, '')
-
+
if self.strip_forward_slash:
name = name.replace('/', '')
-
+
name = name.strip()
-
+
return name
-
-
+
+
class VideoName(PhotoName):
def __init__(self, pref_list):
PhotoName.__init__(self, pref_list)
self.L1_date_check = VIDEO_DATE #used in _get_date_component()
-
+
def _get_metadata_component(self):
"""
Returns portion of video / subfolder name based on the metadata
-
+
Note: date time metadata found in _getDateComponent()
"""
- return get_video_metadata_component(self)
+ return get_video_metadata_component(self)
class PhotoSubfolder(PhotoName):
"""
Generate subfolder names for photo files
"""
-
+
def __init__(self, pref_list):
self.pref_list = pref_list
-
+
self.strip_extraneous_white_space = re.compile(r'\s*%s\s*' % os.sep)
self.strip_initial_period_from_extension = True
self.strip_forward_slash = False
self.L1_date_check = IMAGE_DATE #used in _get_date_component()
self.component = pn.SUBFOLDER_COMPONENT #used in error reporting
-
+
def generate_name(self, rpd_file):
-
+
subfolders = PhotoName.generate_name(self, rpd_file)
-
- # subfolder value must never start with a separator, or else any
- # os.path.join function call will fail to join a subfolder to its
+
+ # subfolder value must never start with a separator, or else any
+ # os.path.join function call will fail to join a subfolder to its
# parent folder
if subfolders:
if subfolders[0] == os.sep:
subfolders = subfolders[1:]
-
+
# remove any spaces before and after a directory name
if subfolders and self.rpd_file.strip_characters:
subfolders = self.strip_extraneous_white_space.sub(os.sep, subfolders)
-
+
return subfolders
-
-
-
+
+
+
class VideoSubfolder(PhotoSubfolder):
"""
Generate subfolder names for video files
"""
-
+
def __init__(self, pref_list):
PhotoSubfolder.__init__(self, pref_list)
self.L1_date_check = VIDEO_DATE #used in _get_date_component()
-
-
+
+
def _get_metadata_component(self):
"""
Returns portion of video / subfolder name based on the metadata
-
+
Note: date time metadata found in _getDateComponent()
"""
- return get_video_metadata_component(self)
-
+ return get_video_metadata_component(self)
+
def get_video_metadata_component(video):
"""
Returns portion of video / subfolder name based on the metadata
@@ -432,7 +432,7 @@ def get_video_metadata_component(video):
This is outside of a class definition because of the inheritence
hierarchy.
"""
-
+
problem = None
if video.L1 == CODEC:
v = video.rpd_file.metadata.codec()
@@ -456,7 +456,7 @@ def get_video_metadata_component(video):
return v
class Sequences:
- """
+ """
Holds sequence numbers and letters used in generating filenames.
"""
def __init__(self, downloads_today_tracker, stored_sequence_no):
@@ -465,58 +465,58 @@ class Sequences:
self.downloads_today_tracker = downloads_today_tracker
self.stored_sequence_no = stored_sequence_no
self.matched_sequences = None
-
+
def set_matched_sequence_value(self, matched_sequences):
self.matched_sequences = matched_sequences
-
+
def get_session_sequence_no(self):
if self.matched_sequences is not None:
return self.matched_sequences.session_sequence_no
else:
return self._get_session_sequence_no()
-
+
def _get_session_sequence_no(self):
return self.session_sequence_no + 1
-
+
def get_sequence_letter(self):
if self.matched_sequences is not None:
return self.matched_sequences.sequence_letter
else:
return self._get_sequence_letter()
-
+
def _get_sequence_letter(self):
return self.sequence_letter + 1
-
+
def increment(self, uses_session_sequece_no, uses_sequence_letter):
if uses_session_sequece_no:
self.session_sequence_no += 1
if uses_sequence_letter:
self.sequence_letter += 1
-
+
def get_downloads_today(self):
if self.matched_sequences is not None:
return self.matched_sequences.downloads_today
else:
return self._get_downloads_today()
-
+
def _get_downloads_today(self):
v = self.downloads_today_tracker.get_downloads_today()
if v == -1:
return 1
else:
return v + 1
-
+
def get_stored_sequence_no(self):
if self.matched_sequences is not None:
return self.matched_sequences.stored_sequence_no
else:
return self._get_stored_sequence_no()
-
+
def _get_stored_sequence_no(self):
# Must add 1 to the value, for historic reasons (that is how it used
# to work)
return self.stored_sequence_no + 1
-
+
def create_matched_sequences(self):
sequences = collections.namedtuple(
'AssignedSequences',
diff --git a/rapid/glade3/about.ui b/rapid/glade3/about.ui
index b770f72..221c62e 100644
--- a/rapid/glade3/about.ui
+++ b/rapid/glade3/about.ui
@@ -28,6 +28,7 @@ Martin Dahl Moe
Marco de Freitas &lt;marcodefreitas@gmail.com&gt;
Martin Egger &lt;martin.egger@gmx.net&gt;
Tauno Erik &lt;tauno.erik@gmail.com&gt;
+Sergiy Gavrylov &lt;sergiovana@bigmir.net&gt;
Emanuele Grande &lt;caccolangrifata@gmail.com&gt;
Torben Gundtofte-Bruun &lt;torben@g-b.dk&gt;
Miroslav Matejaš &lt;silverspace@ubuntu-hr.org&gt;
@@ -43,7 +44,7 @@ Michal Predotka &lt;mpredotka@googlemail.com&gt;
Ye Qing &lt;allen19920930@gmail.com&gt;
Luca Reverberi &lt;thereve@gmail.com&gt;
Mikko Ruohola &lt;polarfox@polarfox.net&gt;
-Sergiy Gavrylov &lt;sergiovana@bigmir.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;
diff --git a/rapid/glade3/xmp.ui b/rapid/glade3/xmp.ui
new file mode 100644
index 0000000..c0fd03b
--- /dev/null
+++ b/rapid/glade3/xmp.ui
@@ -0,0 +1,1376 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="2.24"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkDialog" id="dialog1">
+ <property name="can_focus">False</property>
+ <property name="border_width">5</property>
+ <property name="type_hint">dialog</property>
+ <child internal-child="vbox">
+ <object class="GtkVBox" id="dialog-vbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">2</property>
+ <child internal-child="action_area">
+ <object class="GtkHButtonBox" id="dialog-action_area1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkNotebook" id="notebook">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkTable" id="table1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">10</property>
+ <property name="n_columns">5</property>
+ <child>
+ <object class="GtkLabel" id="label14">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="yes" comments="The Description field, often referred to as &quot;Caption,&quot; should report the who, what and why of what the photograph depicts. Source: http://www.photometadata.org/META-Resources-Field-Guide-to-Metadata#Description">Description:</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">automatic</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <object class="GtkViewport" id="viewport1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkTextView" id="textview1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label15">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes" comments="A headline is a brief, publishable synopsis or summary of the contents of the photograph. Source: http://www.photometadata.org/META-Resources-Field-Guide-to-Metadata#Headline">Headline:</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label16">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="yes" comments="Keyword terms or phrases describe the subject of content in the photograph. Source: http://www.photometadata.org/META-Resources-Field-Guide-to-Metadata#Keywords">Keywords:</property>
+ </object>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow3">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">automatic</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <object class="GtkViewport" id="viewport3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkTextView" id="textview2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label17">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes" comments="The name of the person writing, editing or correcting the description of the photograph. Source: http://www.photometadata.org/META-Resources-Field-Guide-to-Metadata#Description%20writer">Description writer:</property>
+ </object>
+ <packing>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label18">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes" comments="The Copyright Notice for the photo.">Copyright:</property>
+ </object>
+ <packing>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry3">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label19">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes" comments="The location of a web page describing the owner and/or rights statement for this photo.">Copyright URL:</property>
+ </object>
+ <packing>
+ <property name="top_attach">6</property>
+ <property name="bottom_attach">7</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry4">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="invisible_char_set">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">6</property>
+ <property name="bottom_attach">7</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label20">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="yes" comments="Name or names of a person or the people shown in the photo.">Person(s) shown:</property>
+ </object>
+ <packing>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow4">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">automatic</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <object class="GtkViewport" id="viewport4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkTextView" id="textview3">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Description</property>
+ </object>
+ <packing>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTable" id="table3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">16</property>
+ <property name="n_columns">5</property>
+ <child>
+ <object class="GtkLabel" id="label21">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Location Created</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="right_attach">5</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label22">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Sublocation:</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label23">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">City:</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label24">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Province/state:</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label25">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Country name:</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label26">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Country ISO-code:</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label27">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">World region:</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">6</property>
+ <property name="bottom_attach">7</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry5">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="invisible_char_set">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry6">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="invisible_char_set">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry7">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="invisible_char_set">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry8">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="invisible_char_set">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry9">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="invisible_char_set">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry10">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="invisible_char_set">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">6</property>
+ <property name="bottom_attach">7</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label28">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Location Shown</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="right_attach">5</property>
+ <property name="top_attach">8</property>
+ <property name="bottom_attach">9</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label29">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Sublocation:</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">9</property>
+ <property name="bottom_attach">10</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label30">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">City:</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">10</property>
+ <property name="bottom_attach">11</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label31">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Province/state:</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">11</property>
+ <property name="bottom_attach">12</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label32">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Country name:</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">12</property>
+ <property name="bottom_attach">13</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label33">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Country ISO-code:</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">13</property>
+ <property name="bottom_attach">14</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label34">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">World region:</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">14</property>
+ <property name="bottom_attach">15</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry11">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="invisible_char_set">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">9</property>
+ <property name="bottom_attach">10</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry12">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="invisible_char_set">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">10</property>
+ <property name="bottom_attach">11</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry13">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="invisible_char_set">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">11</property>
+ <property name="bottom_attach">12</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry14">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="invisible_char_set">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">12</property>
+ <property name="bottom_attach">13</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry15">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="invisible_char_set">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">13</property>
+ <property name="bottom_attach">14</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry16">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">•</property>
+ <property name="invisible_char_set">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">True</property>
+ <property name="secondary_icon_sensitive">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">14</property>
+ <property name="bottom_attach">15</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkButton" id="copy_location_button">
+ <property name="label" translatable="yes">Copy to Location Shown</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_action_appearance">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">7</property>
+ <property name="bottom_attach">8</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label35">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Location</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTable" id="table2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">10</property>
+ <property name="n_columns">2</property>
+ <property name="column_spacing">12</property>
+ <property name="row_spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes" comments="Person who created the photograph">Creator:</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes" comments="Job title of the person who created the photograph">Creator's job title:</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="yes" comments="Address of the person who created the photograph">Address:</property>
+ </object>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">City:</property>
+ </object>
+ <packing>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes" comments="State or province of the person who created the photograph">State/province:</property>
+ </object>
+ <packing>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes" comments="Postal code of the person who created the photograph">Postal code:</property>
+ </object>
+ <packing>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label10">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes" comments="Country of the person who created the photograph">Country: </property>
+ </object>
+ <packing>
+ <property name="top_attach">6</property>
+ <property name="bottom_attach">7</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label11">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes" comments="Phone numbers / number of the person who created the photograph">Phone(s):</property>
+ </object>
+ <packing>
+ <property name="top_attach">7</property>
+ <property name="bottom_attach">8</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label12">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes" comments="Email address / addresses of the person who created the photograph">Email(s):</property>
+ </object>
+ <packing>
+ <property name="top_attach">8</property>
+ <property name="bottom_attach">9</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label13">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes" comments="Websites / website of the person who created the photograph">Website(s):</property>
+ </object>
+ <packing>
+ <property name="top_attach">9</property>
+ <property name="bottom_attach">10</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="creator_combobox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="has_entry">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="job_title_combobox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="has_entry">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="city_combobox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="has_entry">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="state_combobox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="has_entry">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="postal_code_combobox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="has_entry">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="country_combobox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="has_entry">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">6</property>
+ <property name="bottom_attach">7</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="phone_combobox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="has_entry">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">7</property>
+ <property name="bottom_attach">8</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="email_combobox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="has_entry">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">8</property>
+ <property name="bottom_attach">9</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="website_combobox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="has_entry">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">9</property>
+ <property name="bottom_attach">10</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">automatic</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <object class="GtkViewport" id="viewport2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkTextView" id="address_textview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes" comments="Contact information for the photographer">Photographer</property>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/rapid/rapid.py b/rapid/rapid.py
index fab5796..7de18c2 100755
--- a/rapid/rapid.py
+++ b/rapid/rapid.py
@@ -18,6 +18,7 @@
### Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
### USA
+use_pynotify = True
import tempfile
@@ -39,7 +40,9 @@ import webbrowser
import sys, time, types, os, datetime
import gobject, pango, cairo, array, pangocairo, gio
-import pynotify
+
+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
@@ -50,7 +53,7 @@ logger = log_to_stderr()
# Rapid Photo Downloader modules
import rpdfile
-
+
import problemnotification as pn
import thumbnail as tn
import rpdmultiprocessing as rpdmp
@@ -105,10 +108,10 @@ from config import STATUS_CANNOT_DOWNLOAD, STATUS_DOWNLOADED, \
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
+#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
@@ -117,12 +120,12 @@ def date_time_human_readable(date, with_line_break=True):
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"),
+ {'date':date.strftime("%x"),
'hour':date.strftime("%H"),
- 'minute':date.strftime("%M"),
+ 'minute':date.strftime("%M"),
'second':date.strftime("%S"),
'subsecond': subseconds}
@@ -135,7 +138,7 @@ class DeviceCollection(gtk.TreeView):
def __init__(self, parent_app):
self.parent_app = parent_app
- # device icon & name, size of images on the device (human readable),
+ # 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,
@@ -144,15 +147,15 @@ class DeviceCollection(gtk.TreeView):
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,
+
+
+ # 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()
@@ -168,46 +171,46 @@ class DeviceCollection(gtk.TreeView):
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"),
+
+ 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,
+ 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,
@@ -216,9 +219,9 @@ class DeviceCollection(gtk.TreeView):
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
@@ -228,7 +231,8 @@ class DeviceCollection(gtk.TreeView):
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
@@ -238,39 +242,39 @@ class DeviceCollection(gtk.TreeView):
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
+ 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
+ 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()
@@ -278,16 +282,16 @@ class DeviceCollection(gtk.TreeView):
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
@@ -316,14 +320,14 @@ class DeviceCollection(gtk.TreeView):
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()
@@ -332,13 +336,14 @@ class DeviceCollection(gtk.TreeView):
logger.debug("%s successfully unmounted" % name)
except gio.Error, inst:
logger.error("%s did not unmount: %s", name, inst)
-
- title = _("%(device)s did not unmount") % {'device': name}
- message = '%s' % inst
-
- n = pynotify.Notification(title, message)
- n.set_icon_from_pixbuf(self.parent_app.application_icon)
- n.show()
+
+ 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)
+ n.show()
def create_cairo_image_surface(pil_image, image_width, image_height):
@@ -353,55 +358,55 @@ class ThumbnailCellRenderer(gtk.CellRenderer):
__gproperties__ = {
"image": (gobject.TYPE_PYOBJECT, "Image",
"Image", gobject.PARAM_READWRITE),
-
- "filename": (gobject.TYPE_STRING, "Filename",
+
+ "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
+
+ #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:
@@ -416,33 +421,33 @@ class ThumbnailCellRenderer(gtk.CellRenderer):
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()
-
+ #~ 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
@@ -453,7 +458,7 @@ class ThumbnailCellRenderer(gtk.CellRenderer):
attr.insert(font_family)
layout.set_attributes(attr)
- layout.set_text(self.filename)
+ layout.set_text(self.filename)
context.move_to(text_x, text_y)
context.show_layout(layout)
@@ -461,13 +466,13 @@ class ThumbnailCellRenderer(gtk.CellRenderer):
#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):
@@ -475,39 +480,39 @@ class ThumbnailDisplay(gtk.IconView):
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.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
@@ -515,11 +520,11 @@ class ThumbnailDisplay(gtk.IconView):
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
@@ -529,11 +534,11 @@ class ThumbnailDisplay(gtk.IconView):
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)
@@ -542,12 +547,12 @@ class ThumbnailDisplay(gtk.IconView):
#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
@@ -562,8 +567,8 @@ class ThumbnailDisplay(gtk.IconView):
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
@@ -582,19 +587,19 @@ class ThumbnailDisplay(gtk.IconView):
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)
+ 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
@@ -616,33 +621,33 @@ class ThumbnailDisplay(gtk.IconView):
else:
logger.critical("FIXME: unknown status: %s", status)
status_icon = self.not_downloaded_icon
- return status_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):
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)
@@ -650,7 +655,7 @@ class ThumbnailDisplay(gtk.IconView):
scan_pid = rpd_file.scan_pid
timestamp = int(rpd_file.modification_time)
-
+
iter = self.liststore.append((thumbnail_icon,
True,
unique_id,
@@ -661,47 +666,47 @@ class ThumbnailDisplay(gtk.IconView):
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
+ """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):
+
+ 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
@@ -711,13 +716,13 @@ class ThumbnailDisplay(gtk.IconView):
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 unique_id is not None:
iter = self.get_iter_from_unique_id(unique_id)
@@ -734,31 +739,31 @@ class ThumbnailDisplay(gtk.IconView):
path = 0
iter = self.liststore.get_iter(path)
unique_id = self.get_unique_id_from_iter(iter)
-
-
- rpd_file = self.rpd_files[unique_id]
-
+
+
+ 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:
+ 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,
+ 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:
@@ -766,44 +771,44 @@ class ThumbnailDisplay(gtk.IconView):
else:
row -= 1
iter = self.liststore.get_iter(row)
- return iter
-
+ 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]:
@@ -813,7 +818,7 @@ class ThumbnailDisplay(gtk.IconView):
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
@@ -825,12 +830,12 @@ class ThumbnailDisplay(gtk.IconView):
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()
@@ -853,7 +858,7 @@ class ThumbnailDisplay(gtk.IconView):
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
@@ -865,17 +870,17 @@ class ThumbnailDisplay(gtk.IconView):
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
+ 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):
"""
@@ -895,19 +900,19 @@ class ThumbnailDisplay(gtk.IconView):
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)
@@ -915,7 +920,7 @@ class ThumbnailDisplay(gtk.IconView):
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
"""
@@ -923,81 +928,81 @@ class ThumbnailDisplay(gtk.IconView):
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)
-
+ 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
+ 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:
@@ -1006,10 +1011,10 @@ class ThumbnailDisplay(gtk.IconView):
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
@@ -1019,24 +1024,24 @@ class ThumbnailDisplay(gtk.IconView):
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")
@@ -1045,7 +1050,7 @@ class ThumbnailDisplay(gtk.IconView):
self.treerow_index = {}
self.process_index = {}
-
+
self.rpd_files = {}
else:
if scan_pid in self.process_index:
@@ -1060,80 +1065,80 @@ 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):
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,
+
+ 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
@@ -1143,7 +1148,7 @@ class TaskManager:
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
@@ -1153,29 +1158,29 @@ class TaskManager:
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.
-
+ 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,
+ 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
@@ -1188,38 +1193,38 @@ class TaskManager:
class ScanManager(TaskManager):
-
- def __init__(self, results_callback, batch_size,
+
+ 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,
+
+ 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,
+ 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,
+ 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
+ # 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,
+
+ 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]
@@ -1227,25 +1232,25 @@ class CopyFilesManager(TaskManager):
files = task[3]
modify_files_during_download = task[4]
modify_pipe = task[5]
-
+
copy_files = copyfiles.CopyFiles(photo_download_folder,
video_download_folder,
- files,
+ files,
modify_files_during_download,
modify_pipe,
- scan_pid, self.batch_size,
+ 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,
+ 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))
@@ -1256,44 +1261,44 @@ class FileModifyManager(TaskManager):
Duplex, multiprocess, similar to BackupFilesManager
"""
def __init__(self, results_callback):
- TaskManager.__init__(self, results_callback=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,
+
+ 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]
-
+
file_modify = filemodify.FileModify(auto_rotate_jpeg, focal_length,
- task_process_conn, terminate_queue,
+ task_process_conn, terminate_queue,
run_event)
file_modify.start()
- self._processes.append((file_modify, terminate_queue, run_event,
+ 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):
@@ -1302,30 +1307,31 @@ class BackupFilesManager(TaskManager):
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))
-
- def _initiate_task(self, task, task_results_conn, task_process_conn,
+ p[3].send((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,
+ 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,
+ 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):
+
+ def backup_file(self, move_succeeded, rpd_file, path_suffix,
+ backup_duplicate_overwrite,
+ download_count):
if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO:
logger.debug("Backing up photo %s", rpd_file.download_name)
@@ -1334,124 +1340,124 @@ class BackupFilesManager(TaskManager):
for path in self.backup_devices_by_path:
backup_type = self.backup_devices_by_path[path][2]
- if ((backup_type == PHOTO_VIDEO_BACKUP) or
+ if ((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)):
logger.debug("Backing up to %s", path)
task_results_conn = self.backup_devices_by_path[path][0]
- task_results_conn.send((move_succeeded, rpd_file, path_suffix,
- backup_duplicate_overwrite))
+ task_results_conn.send((move_succeeded, rpd_file, path_suffix,
+ backup_duplicate_overwrite, download_count))
else:
logger.debug("Not backing up to %s", path)
-
+
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):
+ 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
-
+ 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,
+ 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,
+
+ def rename_file_and_move_to_subfolder(self, download_succeeded,
download_count, rpd_file):
-
- self.task_results_conn.send((download_succeeded, download_count,
+
+ logger.debug("Sending file for rename: %s.", download_count)
+ self.task_results_conn.send((download_succeeded, download_count,
rpd_file))
- logger.debug("Download count: %s.", download_count)
-
+
def task_results(self, source, condition):
- move_succeeded, rpd_file = self.task_results_conn.recv()
- self.results_callback(move_succeeded, rpd_file)
+ 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
+
+ x = event.area.x
+ y = event.area.y
w = event.area.width
h = event.area.height
-
- #constrain operations to event area
+
+ #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()
+ 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
@@ -1460,7 +1466,7 @@ class ResizblePilImage(gtk.DrawingArea):
# 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:
@@ -1473,38 +1479,38 @@ class ResizblePilImage(gtk.DrawingArea):
#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()
+ cairo_context.paint()
+
+ return False
+
- 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 = 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,
+
+ def set_preview_image(self, unique_id, pil_image, include_checkbutton_visible=None,
checked=None):
"""
"""
@@ -1516,72 +1522,74 @@ class PreviewImage:
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):
-
+ 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()
- self._init_pynotify()
-
+
+ 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,
+ 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)
@@ -1592,30 +1600,30 @@ class RapidApp(dbus.service.Object):
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()
+ 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
+ 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):
@@ -1626,14 +1634,14 @@ class RapidApp(dbus.service.Object):
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
# # #
@@ -1642,61 +1650,61 @@ class RapidApp(dbus.service.Object):
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()
-
+ 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,
+ 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,
+ 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")
@@ -1704,26 +1712,26 @@ class RapidApp(dbus.service.Object):
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:
+ 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
@@ -1737,35 +1745,36 @@ class RapidApp(dbus.service.Object):
# # #
# 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)
-
-
+ 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
+ Commences the scanning of a device using the preference values for
any paths to ignore while scanning
"""
- return self.scan_manager.add_task([device,
+ 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
+ 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.
"""
@@ -1781,34 +1790,34 @@ class RapidApp(dbus.service.Object):
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)
+ 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,
+
+ 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
+
+ Removes any image media that are currently not downloaded,
+ or finished downloading
"""
-
+
if self.using_volume_monitor():
self.start_volume_monitor()
@@ -1816,10 +1825,10 @@ class RapidApp(dbus.service.Object):
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
@@ -1827,7 +1836,7 @@ class RapidApp(dbus.service.Object):
if not mount.is_shadowed():
path = mount.get_root().get_path()
if path:
- if (path in self.prefs.device_blacklist and
+ if (path in self.prefs.device_blacklist and
self.search_for_PSD()):
logger.info("%s ignored", mount.get_name())
else:
@@ -1835,17 +1844,17 @@ class RapidApp(dbus.service.Object):
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
+ 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
+ # user manually specified the path from which to download
path = self.prefs.device_location
if path:
logger.info("Using manually specified path %s", path)
@@ -1858,35 +1867,47 @@ class RapidApp(dbus.service.Object):
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_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 (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 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.
@@ -1894,8 +1915,8 @@ class RapidApp(dbus.service.Object):
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
+ # 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:
@@ -1908,7 +1929,7 @@ class RapidApp(dbus.service.Object):
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
@@ -1917,20 +1938,28 @@ class RapidApp(dbus.service.Object):
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):
+
+
+ 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,
@@ -1941,37 +1970,37 @@ class RapidApp(dbus.service.Object):
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]
-
+ self.prefs.device_blacklist = [path]
+
def search_for_PSD(self):
"""
- Check to see if user preferences are to automatically search for
+ 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
+ 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)
+ (False, None)
"""
if self.prefs.backup_images:
if self.prefs.backup_device_autodetection:
- # Determine if the auto-detected backup device is
+ # 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
+ # 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)
@@ -1991,7 +2020,7 @@ class RapidApp(dbus.service.Object):
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
+ # 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:
@@ -2013,22 +2042,22 @@ class RapidApp(dbus.service.Object):
#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",
+ 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.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
+
+ 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:
@@ -2043,22 +2072,22 @@ class RapidApp(dbus.service.Object):
# 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
+
+ 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
@@ -2069,7 +2098,7 @@ class RapidApp(dbus.service.Object):
if mount.is_shadowed():
# ignore this type of mount
return
-
+
path = mount.get_root().get_path()
if path is not None:
@@ -2078,7 +2107,7 @@ class RapidApp(dbus.service.Object):
'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
@@ -2087,84 +2116,86 @@ class RapidApp(dbus.service.Object):
self.update_no_backup_devices()
self.display_free_space()
- elif self.prefs.device_autodetection and (dv.is_DCIM_device(path) or
+ elif self.prefs.device_autodetection and (dv.is_DCIM_device(path) or
self.search_for_PSD()):
-
+
self.auto_start_is_on = self.prefs.auto_download_upon_device_insertion
device = dv.Device(path=path, mount=mount)
- if self.search_for_PSD() and path not in self.prefs.device_whitelist:
- # prompt user if device should be used or not
- self.get_use_device(device)
- else:
- scan_pid = self.start_device_scan(device)
- self.mounts_by_path[path] = scan_pid
-
+
+ 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,
+
+ 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()
@@ -2173,18 +2204,18 @@ class RapidApp(dbus.service.Object):
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():
@@ -2192,26 +2223,26 @@ class RapidApp(dbus.service.Object):
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
+ 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()
@@ -2219,64 +2250,64 @@ class RapidApp(dbus.service.Object):
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,
+ 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()
-
+ 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
@@ -2285,28 +2316,28 @@ class RapidApp(dbus.service.Object):
# (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 files during download"""
return self.prefs.auto_rotate_jpeg or (self.focal_length is not None)
-
+
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)
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") % {
@@ -2319,40 +2350,40 @@ class RapidApp(dbus.service.Object):
# set time download is starting if it is not already set
# it is unset when all downloads are completed
if self.download_start_time is None:
- self.download_start_time = datetime.datetime.now()
+ self.download_start_time = datetime.datetime.now()
- # Set status to download pending
+ # 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):
@@ -2360,16 +2391,16 @@ class RapidApp(dbus.service.Object):
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,
+ 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,
+ no_videos_to_download = self.files_of_type_present(files,
rpdfile.FILE_TYPE_VIDEO,
return_file_count=True)
if no_videos_to_download:
@@ -2379,30 +2410,30 @@ class RapidApp(dbus.service.Object):
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,
+ 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
@@ -2414,13 +2445,13 @@ class RapidApp(dbus.service.Object):
else:
modify_pipe = None
-
+
# Initiate copy files process
- self.copy_files_manager.add_task((photo_download_folder,
+ self.copy_files_manager.add_task((photo_download_folder,
video_download_folder, scan_pid,
files, modify_files_during_download,
modify_pipe))
-
+
def copy_files_results(self, source, condition):
"""
Handle results from copy files process
@@ -2434,7 +2465,7 @@ 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)
-
+
# Report which temporary directories are being used for this
# download
if photo_temp_dir and video_temp_dir:
@@ -2445,10 +2476,10 @@ class RapidApp(dbus.service.Object):
photo_temp_dir)
else:
logger.debug("Using temp dir %s (videos)",
- video_temp_dir)
+ 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,
+ 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)
@@ -2457,39 +2488,39 @@ class RapidApp(dbus.service.Object):
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,
+ 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)
@@ -2498,21 +2529,21 @@ class RapidApp(dbus.service.Object):
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
-
+
self.subfolder_file_manager.rename_file_and_move_to_subfolder(
- download_succeeded,
- download_count,
+ 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.
-
+ '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)
@@ -2520,31 +2551,31 @@ class RapidApp(dbus.service.Object):
else:
# Process is complete, i.e. conn_type == rpdmp.CONN_COMPLETE
connection.close()
- return False
+ return False
+
-
def download_is_occurring(self):
- """Returns True if a file is currently being downloaded, renamed or
+ """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):
+
+ 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,
+ self.log_error(config.WARNING, rpd_file.error_title,
rpd_file.error_msg, rpd_file.error_extra_detail)
-
+
if self.prefs.backup_images and len(self.backup_devices):
if self.prefs.backup_device_autodetection:
if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO:
@@ -2553,23 +2584,24 @@ class RapidApp(dbus.service.Object):
path_suffix = self.prefs.video_backup_identifier
else:
path_suffix = None
-
- self.backup_manager.backup_file(move_succeeded, rpd_file,
+
+ self.backup_manager.backup_file(move_succeeded, rpd_file,
path_suffix,
- self.prefs.backup_duplicate_overwrite)
+ self.prefs.backup_duplicate_overwrite,
+ download_count)
else:
self.file_download_finished(move_succeeded, rpd_file)
-
-
+
+
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
+ return ((file_type == rpdfile.FILE_TYPE_PHOTO and
self.no_photo_backup_devices > 1) or
- (file_type == rpdfile.FILE_TYPE_VIDEO and
+ (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
@@ -2578,30 +2610,30 @@ class RapidApp(dbus.service.Object):
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,
+ 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, 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 reply on showing an error message in the
+ # do not want to reply 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):
- self.log_error(config.SERIOUS_ERROR,
- rpd_file.error_title,
+ self.log_error(config.SERIOUS_ERROR,
+ rpd_file.error_title,
rpd_file.error_msg, rpd_file.error_extra_detail)
-
+
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):
@@ -2610,7 +2642,7 @@ class RapidApp(dbus.service.Object):
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
@@ -2619,23 +2651,23 @@ class RapidApp(dbus.service.Object):
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,
+ 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
+ # 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,
+ 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")
@@ -2649,63 +2681,63 @@ class RapidApp(dbus.service.Object):
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())
+ 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
+ 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.
+ # 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)
-
+ 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):
@@ -2714,10 +2746,10 @@ class RapidApp(dbus.service.Object):
f.delete(cancellable=None)
except gio.Error, inst:
logger.error("Failure deleting file %s", file)
- logger.error(inst)
-
+ 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
@@ -2740,14 +2772,14 @@ class RapidApp(dbus.service.Object):
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(
@@ -2759,36 +2791,37 @@ class RapidApp(dbus.service.Object):
no_files_downloaded = no_photos_downloaded + no_videos_downloaded
no_files_failed = no_photos_failed + no_videos_failed
no_warnings = self.download_tracker.get_no_warnings(scan_pid)
-
+
file_types = self.file_types_by_number(no_photos_downloaded, no_videos_downloaded)
file_types_failed = self.file_types_by_number(no_photos_failed, no_videos_failed)
message = _("%(noFiles)s %(filetypes)s downloaded") % \
{'noFiles':no_files_downloaded, 'filetypes': file_types}
-
+
if no_files_failed:
message += "\n" + _("%(noFiles)s %(filetypes)s failed to download") % {'noFiles':no_files_failed, 'filetypes':file_types_failed}
-
+
if no_warnings:
- message = "%s\n%s " % (message, no_warnings) + _("warnings")
-
- n = pynotify.Notification(notification_name, message)
- n.set_icon_from_pixbuf(icon)
-
- n.show()
-
+ message = "%s\n%s " % (message, no_warnings) + _("warnings")
+
+ if use_pynotify:
+ n = pynotify.Notification(notification_name, message)
+ n.set_icon_from_pixbuf(icon)
+
+ n.show()
+
def notify_download_complete(self):
if self.display_summary_notification:
message = _("All downloads complete")
-
+
# photo downloads
photo_downloads = self.download_tracker.total_photos_downloaded
if photo_downloads:
filetype = self.file_types_by_number(photo_downloads, 0)
message += "\n" + _("%(number)s %(numberdownloaded)s") % \
- {'number': photo_downloads,
+ {'number': photo_downloads,
'numberdownloaded': _("%(filetype)s downloaded") % \
{'filetype': filetype}}
-
+
# photo failures
photo_failures = self.download_tracker.total_photo_failures
if photo_failures:
@@ -2797,16 +2830,16 @@ class RapidApp(dbus.service.Object):
{'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,
+ {'number': video_downloads,
'numberdownloaded': _("%(filetype)s downloaded") % \
{'filetype': filetype}}
-
+
# video failures
video_failures = self.download_tracker.total_video_failures
if video_failures:
@@ -2815,65 +2848,66 @@ class RapidApp(dbus.service.Object):
{'number': video_failures,
'numberdownloaded': _("%(filetype)s failed to download") % \
{'filetype': filetype}}
-
+
# warnings
- warnings = self.download_tracker.total_warnings
+ warnings = self.download_tracker.total_warnings
if warnings:
message += "\n" + _("%(number)s %(numberdownloaded)s") % \
- {'number': warnings,
+ {'number': warnings,
'numberdownloaded': _("warnings")}
-
- n = pynotify.Notification(PROGRAM_NAME, message)
- n.set_icon_from_pixbuf(self.application_icon)
- n.show()
+
+ if use_pynotify:
+ n = pynotify.Notification(PROGRAM_NAME, message)
+ n.set_icon_from_pixbuf(self.application_icon)
+ n.show()
self.display_summary_notification = False # don't show it again unless needed
-
-
+
+
def _update_file_download_device_progress(self, scan_pid, unique_id, 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 len(self.backup_devices)):
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,
+ '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,
+ {'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,
+ 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):
"""
@@ -2882,10 +2916,10 @@ class RapidApp(dbus.service.Object):
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
@@ -2897,10 +2931,10 @@ class RapidApp(dbus.service.Object):
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)
@@ -2916,16 +2950,16 @@ class RapidApp(dbus.service.Object):
except gio.Error, inst:
logger.error("Failure deleting temporary folder %s", directory)
logger.error(inst)
-
-
- # # #
+
+
+ # # #
# Preferences
# # #
-
-
- def _init_prefs(self, auto_detect, device_location):
+
+
+ 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:
@@ -2933,82 +2967,82 @@ class RapidApp(dbus.service.Object):
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
+
+ # 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
+
+ # 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 /
+
+ # 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")
+ logger.info("No downloads have occurred so far today")
- self.downloads_today_value = Value(c_int,
+ 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.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()
-
+ 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
+ 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
+ # 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",
+ 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
@@ -3017,90 +3051,89 @@ class RapidApp(dbus.service.Object):
if key == 'show_log_dialog':
self.menu_log_window.set_active(value)
- elif key in ['device_autodetection', 'device_autodetection_psd',
+ 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',
+
+
+ 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()
+ 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.")
- #~ sys.exit(1)
do_not_size_icon = False
- self.notification_icon_size = 48
+ self.notification_icon_size = 48
try:
info = pynotify.get_server_info()
except:
@@ -3111,7 +3144,7 @@ class RapidApp(dbus.service.Object):
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'))
@@ -3137,7 +3170,7 @@ class RapidApp(dbus.service.Object):
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")
@@ -3148,71 +3181,71 @@ class RapidApp(dbus.service.Object):
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)
-
+ 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()
@@ -3229,32 +3262,32 @@ class RapidApp(dbus.service.Object):
_("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.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)
@@ -3265,7 +3298,7 @@ class RapidApp(dbus.service.Object):
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)
@@ -3278,7 +3311,7 @@ class RapidApp(dbus.service.Object):
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)
@@ -3290,27 +3323,27 @@ class RapidApp(dbus.service.Object):
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_button.connect("toggled", self.on_copy_button_toggle_event)
+
self.copy_toolbar.show_all()
- self._set_copy_toolbar_active_arrows()
-
+ 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(
@@ -3336,9 +3369,9 @@ class RapidApp(dbus.service.Object):
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.to_photo_filechooser_button.connect("selection-changed",
self.on_to_photo_filechooser_button_selection_changed)
self.to_video_filechooser_button.connect("selection-changed",
self.on_to_video_filechooser_button_selection_changed)
@@ -3346,14 +3379,14 @@ class RapidApp(dbus.service.Object):
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)
@@ -3377,36 +3410,36 @@ class RapidApp(dbus.service.Object):
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 = filechooserbutton.get_current_folder()
if path and not self.rerun_setup_available_image_and_video_media:
self.prefs.device_location = path
-
+
def on_to_photo_filechooser_button_selection_changed(self, filechooserbutton):
path = filechooserbutton.get_current_folder()
if path:
self.prefs.download_folder = path
-
+
def on_to_video_filechooser_button_selection_changed(self, filechooserbutton):
path = filechooserbutton.get_current_folder()
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)
@@ -3419,20 +3452,20 @@ class RapidApp(dbus.service.Object):
def _set_window_size(self):
"""
Remember the window size from the last time the program was run, or
- set a default size
+ set a default size
"""
-
+
if self.prefs.main_window_maximized:
self.rapidapp.maximize()
- self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH,
+ 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,
+ self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH,
config.DEFAULT_WINDOW_HEIGHT)
-
+
def _set_device_collection_size(self):
"""
@@ -3445,36 +3478,36 @@ class RapidApp(dbus.service.Object):
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]
+ 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
@@ -3486,7 +3519,7 @@ class RapidApp(dbus.service.Object):
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
@@ -3495,20 +3528,20 @@ class RapidApp(dbus.service.Object):
"""
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
+ 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)
@@ -3521,22 +3554,22 @@ class RapidApp(dbus.service.Object):
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
+ Displays the amount of space free on the filesystem the files will be
downloaded to.
-
- Also displays backup volumes / path being used.
+
+ 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)
@@ -3545,17 +3578,17 @@ class RapidApp(dbus.service.Object):
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])
@@ -3563,13 +3596,13 @@ class RapidApp(dbus.service.Object):
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
+ #(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
+ #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
+ #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
@@ -3579,17 +3612,17 @@ class RapidApp(dbus.service.Object):
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).
+ #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 self.prefs.backup_images:
if not self.prefs.backup_device_autodetection:
if self.prefs.backup_location == self.prefs.backup_video_location:
if DOWNLOAD_VIDEO:
@@ -3604,29 +3637,29 @@ class RapidApp(dbus.service.Object):
'path':self.prefs.backup_location,
'path2': self.prefs.backup_video_location}
else:
- msg2 = self.display_backup_mounts()
-
+ 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()
-
-
+ self.error_log.widget.show()
+
+
def on_menu_log_window_toggled(self, widget):
active = widget.get_active()
self.prefs.show_log_dialog = active
@@ -3634,14 +3667,14 @@ class RapidApp(dbus.service.Object):
self.error_log.widget.show()
else:
self.error_log.widget.hide()
-
+
def notify_prefs_are_invalid(self, details):
title = _("Program preferences are invalid")
logger.critical(title)
self.log_error(severity=config.CRITICAL_ERROR, problem=title,
details=details)
-
-
+
+
# # #
# Utility functions
# # #
@@ -3650,7 +3683,7 @@ class RapidApp(dbus.service.Object):
"""
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
"""
@@ -3665,7 +3698,7 @@ class RapidApp(dbus.service.Object):
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
@@ -3679,12 +3712,12 @@ class RapidApp(dbus.service.Object):
video_size += rpd_file.size
return (photo_size, video_size)
-
+
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.
"""
@@ -3702,27 +3735,27 @@ class RapidApp(dbus.service.Object):
if not need_video_folder:
if self.files_of_type_present(files, rpdfile.FILE_TYPE_VIDEO):
need_video_folder = True
-
+
# second, check validity
if need_photo_folder:
- if not self.is_valid_download_dir(self.prefs.download_folder,
+ if not self.is_valid_download_dir(self.prefs.download_folder,
is_photo_dir=True):
valid = False
invalid_dirs.append(self.prefs.download_folder)
else:
- logger.debug("Photo download folder is valid: %s",
+ 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):
+ is_photo_dir=False):
valid = False
invalid_dirs.append(self.prefs.video_download_folder)
else:
- logger.debug("Video download folder is valid: %s",
+ logger.debug("Video download folder is valid: %s",
self.prefs.video_download_folder)
-
+
return (valid, invalid_dirs)
def same_file_system(self, file1, file2):
@@ -3735,27 +3768,27 @@ class RapidApp(dbus.service.Object):
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
@@ -3765,11 +3798,11 @@ class RapidApp(dbus.service.Object):
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",
+ logger.error("%s download folder does not exist: %s",
download_folder_type, path)
if show_error_in_log:
severity = config.WARNING
@@ -3781,16 +3814,16 @@ class RapidApp(dbus.service.Object):
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",
+ 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)
+ self.log_error(severity, problem, details)
else:
# is the directory writable?
try:
@@ -3803,7 +3836,7 @@ class RapidApp(dbus.service.Object):
problem = _("%(file_type)s download folder is not writable") % {
'file_type': download_folder_type}
details = _("Folder: %s") % path
- self.log_error(severity, problem, details)
+ self.log_error(severity, problem, details)
else:
f = gio.File(temp_dir)
f.delete(cancellable=None)
@@ -3811,66 +3844,66 @@ class RapidApp(dbus.service.Object):
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.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
+
+ # 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.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.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
@@ -3883,7 +3916,7 @@ class RapidApp(dbus.service.Object):
self.device_collection.update_device(scan_pid, size)
self.device_collection.update_progress(scan_pid, 0.0, results_summary, 0, 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"))
@@ -3897,7 +3930,7 @@ class RapidApp(dbus.service.Object):
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:
@@ -3910,20 +3943,20 @@ class RapidApp(dbus.service.Object):
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,
+ 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):
@@ -3932,14 +3965,14 @@ class RapidApp(dbus.service.Object):
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.
+ # 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'))
@@ -3951,26 +3984,26 @@ def start():
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]=='/':
@@ -3987,15 +4020,15 @@ def start():
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:
@@ -4017,7 +4050,7 @@ 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:
+ 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:
@@ -4025,8 +4058,8 @@ def start():
print "Rapid Photo Downloader is already running"
object = bus.get_object (config.DBUS_NAME, "/")
app = dbus.Interface (object, config.DBUS_NAME)
-
- app.start()
+
+ app.start()
if __name__ == "__main__":
start()
diff --git a/rapid/rpdfile.py b/rapid/rpdfile.py
index cf38ebc..a405782 100644
--- a/rapid/rpdfile.py
+++ b/rapid/rpdfile.py
@@ -43,7 +43,8 @@ import thumbnail as tn
RAW_EXTENSIONS = ['arw', 'dcr', 'cr2', 'crw', 'dng', 'mos', 'mef', 'mrw',
- 'nef', 'orf', 'pef', 'raf', 'raw', 'rw2', 'sr2', 'srw']
+ 'nef', 'nrw', 'orf', 'pef', 'raf', 'raw', 'rw2', 'sr2',
+ 'srw']
JPEG_EXTENSIONS = ['jpg', 'jpe', 'jpeg']
diff --git a/rapid/subfolderfile.py b/rapid/subfolderfile.py
index a7b0f89..4e23309 100644
--- a/rapid/subfolderfile.py
+++ b/rapid/subfolderfile.py
@@ -26,6 +26,8 @@ Runs as a daemon process.
import os, datetime, collections
+import shutil
+import errno
import gio
import multiprocessing
import logging
@@ -44,7 +46,7 @@ from gettext import gettext as _
class SyncRawJpeg:
def __init__(self):
self.photos = {}
-
+
def add_download(self, name, extension, date_time, sub_seconds, sequence_number_used):
if name not in self.photos:
self.photos[name] = ([extension], date_time, sub_seconds, sequence_number_used)
@@ -52,16 +54,16 @@ class SyncRawJpeg:
if extension not in self.photos[name][0]:
self.photos[name][0].append(extension)
-
+
def matching_pair(self, name, extension, date_time, sub_seconds):
"""Checks to see if the image matches an image that has already been downloaded.
Image name (minus extension), exif date time, and exif subseconds are checked.
-
+
Returns -1 and a sequence number if the name, extension, and exif values match (i.e. it has already been downloaded)
Returns 0 and a sequence number if name and exif values match, but the extension is different (i.e. a matching RAW + JPG image)
Returns -99 and a sequence number of None if photos detected with the same filenames, but taken at different times
Returns 1 and a sequence number of None if no match"""
-
+
if name in self.photos:
if self.photos[name][1] == date_time and self.photos[name][2] == sub_seconds:
if extension in self.photos[name][0]:
@@ -71,37 +73,37 @@ class SyncRawJpeg:
else:
return (-99, None)
return (1, None)
-
+
def ext_exif_date_time(self, name):
"""Returns first extension, exif date time and subseconds data for the already downloaded photo"""
return (self.photos[name][0][0], self.photos[name][1], self.photos[name][2])
-
+
def time_subseconds_human_readable(date, subseconds):
return _("%(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \
{'hour':date.strftime("%H"),
- 'minute':date.strftime("%M"),
+ 'minute':date.strftime("%M"),
'second':date.strftime("%S"),
- 'subsecond': subseconds}
+ 'subsecond': subseconds}
def load_metadata(rpd_file, temp_file=True):
"""
Loads the metadata for the file. Returns True if operation succeeded, false
otherwise
-
+
If temp_file is true, the the metadata from the temporary file rather than
the original source file is used. This is important, because the metadata
can be modified by the filemodify process.
"""
- if rpd_file.metadata is None:
+ if rpd_file.metadata is None:
if not rpd_file.load_metadata(temp_file):
# Error in reading metadata
rpd_file.add_problem(None, pn.CANNOT_DOWNLOAD_BAD_METADATA, {'filetype': rpd_file.title_capitalized})
return False
return True
-
+
def _generate_name(generator, rpd_file):
-
+
do_generation = True
if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO:
do_generation = load_metadata(rpd_file)
@@ -115,37 +117,37 @@ def _generate_name(generator, rpd_file):
value = ''
else:
value = ''
-
+
return value
def generate_subfolder(rpd_file):
-
+
if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO:
generator = gn.PhotoSubfolder(rpd_file.subfolder_pref_list)
else:
generator = gn.VideoSubfolder(rpd_file.subfolder_pref_list)
-
+
rpd_file.download_subfolder = _generate_name(generator, rpd_file)
return rpd_file
-
+
def generate_name(rpd_file):
do_generation = True
-
+
if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO:
generator = gn.PhotoName(rpd_file.name_pref_list)
else:
generator = gn.VideoName(rpd_file.name_pref_list)
-
+
rpd_file.download_name = _generate_name(generator, rpd_file)
return rpd_file
-
+
class SubfolderFile(multiprocessing.Process):
def __init__(self, results_pipe, sequence_values):
multiprocessing.Process.__init__(self)
self.daemon = True
self.results_pipe = results_pipe
-
+
self.downloads_today = sequence_values[0]
self.downloads_today_date = sequence_values[1]
self.day_start = sequence_values[2]
@@ -154,12 +156,17 @@ class SubfolderFile(multiprocessing.Process):
self.uses_stored_sequence_no = sequence_values[5]
self.uses_session_sequece_no = sequence_values[6]
self.uses_sequence_letter = sequence_values[7]
-
+ # 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 = False
+
logger.debug("Start of day is set to %s", self.day_start.value)
-
+
def progress_callback_no_update(self, amount_downloaded, total):
pass
-
+ #~ if debug_progress:
+ #~ logger.debug("%.1f", amount_downloaded / float(total))
+
def file_exists(self, rpd_file, identifier=None):
"""
Notify user that the download file already exists
@@ -171,24 +178,24 @@ class SubfolderFile(multiprocessing.Process):
date = dt.strftime("%x")
time = dt.strftime("%X")
except:
- logger.warning("Could not determine the file modification time of %s",
+ logger.warning("Could not determine the file modification time of %s",
rpd_file.download_full_file_name)
date = time = ''
-
+
if not identifier:
- rpd_file.add_problem(None, pn.FILE_ALREADY_EXISTS_NO_DOWNLOAD,
+ rpd_file.add_problem(None, pn.FILE_ALREADY_EXISTS_NO_DOWNLOAD,
{'filetype':rpd_file.title_capitalized})
- rpd_file.add_extra_detail(pn.EXISTING_FILE,
- {'filetype': rpd_file.title,
+ rpd_file.add_extra_detail(pn.EXISTING_FILE,
+ {'filetype': rpd_file.title,
'date': date, 'time': time})
rpd_file.status = config.STATUS_DOWNLOAD_FAILED
rpd_file.error_extra_detail = pn.extra_detail_definitions[pn.EXISTING_FILE] % \
{'date':date, 'time':time, 'filetype': rpd_file.title}
else:
- rpd_file.add_problem(None, pn.UNIQUE_IDENTIFIER_ADDED,
+ rpd_file.add_problem(None, pn.UNIQUE_IDENTIFIER_ADDED,
{'filetype':rpd_file.title_capitalized})
- rpd_file.add_extra_detail(pn.UNIQUE_IDENTIFIER,
- {'identifier': identifier,
+ rpd_file.add_extra_detail(pn.UNIQUE_IDENTIFIER,
+ {'identifier': identifier,
'filetype': rpd_file.title,
'date': date, 'time': time})
rpd_file.status = config.STATUS_DOWNLOADED_WITH_WARNING
@@ -197,10 +204,10 @@ class SubfolderFile(multiprocessing.Process):
'date': date, 'time': time}
rpd_file.error_title = rpd_file.problem.get_title()
rpd_file.error_msg = _("Source: %(source)s\nDestination: %(destination)s") \
- % {'source': rpd_file.full_file_name,
+ % {'source': rpd_file.full_file_name,
'destination': rpd_file.download_full_file_name}
return rpd_file
-
+
def download_failure_file_error(self, rpd_file, inst):
"""
Handle cases where file failed to download
@@ -209,24 +216,49 @@ class SubfolderFile(multiprocessing.Process):
rpd_file.add_extra_detail(pn.DOWNLOAD_COPYING_ERROR_DETAIL, inst)
rpd_file.status = config.STATUS_DOWNLOAD_FAILED
logger.error("Failed to create file %s: %s", rpd_file.download_full_file_name, inst)
-
+
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}
-
+
return rpd_file
-
+
+ def download_file_exists(self, rpd_file):
+ """
+ Check how to handle a download file already existing
+ """
+ if (rpd_file.download_conflict_resolution ==
+ config.ADD_UNIQUE_IDENTIFIER):
+ add_unique_identifier = True
+ logger.debug("Will add unique identifier to avoid duplicate filename")
+ else:
+ rpd_file = self.file_exists(rpd_file)
+ add_unique_identifier = False
+ return (rpd_file, add_unique_identifier)
+
+ def added_unique_identifier(self, rpd_file):
+ """
+ Track fact that a unique identifier was added to a file name
+ """
+ move_succeeded = True
+ suffix_already_used = False
+ rpd_file = self.file_exists(rpd_file, identifier)
+ logger.error("%s: %s - %s", rpd_file.full_file_name,
+ rpd_file.problem.get_title(),
+ rpd_file.problem.get_problems())
+ return (rpd_file, move_succeeded, suffix_already_used)
+
def same_name_different_exif(self, sync_photo_name, rpd_file):
"""Notify the user that a file was already downloaded with the same name, but the exif information was different"""
i1_ext, i1_date_time, i1_subseconds = self.sync_raw_jpeg.ext_exif_date_time(sync_photo_name)
- detail = {'image1': "%s%s" % (sync_photo_name, i1_ext),
+ detail = {'image1': "%s%s" % (sync_photo_name, i1_ext),
'image1_date': i1_date_time.strftime("%x"),
- 'image1_time': time_subseconds_human_readable(i1_date_time, i1_subseconds),
- 'image2': rpd_file.name,
+ 'image1_time': time_subseconds_human_readable(i1_date_time, i1_subseconds),
+ 'image2': rpd_file.name,
'image2_date': rpd_file.metadata.date_time().strftime("%x"),
'image2_time': time_subseconds_human_readable(
- rpd_file.metadata.date_time(),
+ rpd_file.metadata.date_time(),
rpd_file.metadata.sub_seconds())}
rpd_file.add_problem(None, pn.SAME_FILE_DIFFERENT_EXIF, detail)
@@ -234,8 +266,8 @@ class SubfolderFile(multiprocessing.Process):
rpd_file.error_msg = pn.problem_definitions[pn.SAME_FILE_DIFFERENT_EXIF][1] % detail
rpd_file.status = config.STATUS_DOWNLOADED_WITH_WARNING
return rpd_file
-
-
+
+
def run(self):
"""
Get subfolder and name.
@@ -246,37 +278,39 @@ class SubfolderFile(multiprocessing.Process):
"""
i = 0
download_count = 0
-
+
duplicate_files = {}
- # Track downloads today, using a class whose purpose is to
+ # Track downloads today, using a class whose purpose is to
# take the value in the user prefs, increment, and then be used
# to update the prefs (which can only happen via the main process)
self.downloads_today_tracker = prefsrapid.DownloadsTodayTracker(
day_start = self.day_start.value,
downloads_today = self.downloads_today.value,
downloads_today_date = self.downloads_today_date.value)
-
+
# Track sequences using shared downloads today and stored sequence number
# (shared with main process)
- self.sequences = gn.Sequences(self.downloads_today_tracker,
+ self.sequences = gn.Sequences(self.downloads_today_tracker,
self.stored_sequence_no.value)
-
+
self.sync_raw_jpeg = SyncRawJpeg()
-
+
while True:
- logger.debug("Finished %s. Getting next task.", download_count)
+ if download_count:
+ logger.debug("Finished %s. Getting next task.", download_count)
- # rename file and move to generated subfolder
+ # rename file and move to generated subfolder
download_succeeded, download_count, rpd_file = self.results_pipe.recv()
-
+
move_succeeded = False
-
+
if download_succeeded:
- temp_file = gio.File(rpd_file.temp_full_file_name)
+ if self.use_gnome_file_operations:
+ temp_file = gio.File(rpd_file.temp_full_file_name)
synchronize_raw_jpg_failed = False
if not (rpd_file.synchronize_raw_jpg and
@@ -290,8 +324,8 @@ class SubfolderFile(multiprocessing.Process):
synchronize_raw_jpg_failed = True
else:
j, sequence_to_use = self.sync_raw_jpeg.matching_pair(
- name=sync_photo_name, extension=sync_photo_ext,
- date_time=rpd_file.metadata.date_time(),
+ name=sync_photo_name, extension=sync_photo_ext,
+ date_time=rpd_file.metadata.date_time(),
sub_seconds=rpd_file.metadata.sub_seconds())
if j == -1:
# this exact file has already been downloaded (same extension, same filename, and same exif date time subsecond info)
@@ -306,26 +340,29 @@ class SubfolderFile(multiprocessing.Process):
self.sequences.set_matched_sequence_value(sequence_to_use)
if j == -99:
rpd_file = self.same_name_different_exif(sync_photo_name, rpd_file)
-
+
if synchronize_raw_jpg_failed:
generation_succeeded = False
else:
# Generate subfolder name and new file name
generation_succeeded = True
-
+
if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO:
if hasattr(rpd_file, 'new_focal_length'):
# A RAW file has had its focal length and aperture adjusted.
# These have been written out to an XMP sidecar, but they won't
- # be picked up by pyexiv2. So temporarily change the values inplace here,
+ # be picked up by pyexiv2. So temporarily change the values inplace here,
# without saving them.
if load_metadata(rpd_file):
rpd_file.metadata["Exif.Photo.FocalLength"] = rpd_file.new_focal_length
rpd_file.metadata["Exif.Photo.FNumber"] = rpd_file.new_aperture
-
+
rpd_file = generate_subfolder(rpd_file)
+
+
if rpd_file.download_subfolder:
-
+ logger.debug("Generated subfolder name %s for file %s", rpd_file.download_subfolder, rpd_file.name)
+
if self.refresh_downloads_today.value:
# overwrite downloads today value tracked here,
# as user has modified their preferences
@@ -333,21 +370,26 @@ class SubfolderFile(multiprocessing.Process):
self.downloads_today_tracker.set_raw_downloads_today_date(self.downloads_today_date.value)
self.downloads_today_tracker.day_start = self.day_start.value
self.refresh_downloads_today.value = False
-
+
# update whatever the stored value is
self.sequences.stored_sequence_no = self.stored_sequence_no.value
rpd_file.sequences = self.sequences
-
+
# generate the file name
rpd_file = generate_name(rpd_file)
-
+
if rpd_file.has_problem():
+ logger.debug("Encountered a problem generating file name for file %s", rpd_file.name)
rpd_file.status = config.STATUS_DOWNLOADED_WITH_WARNING
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}
-
+ else:
+ logger.debug("Generated file name %s for file %s", rpd_file.download_name, rpd_file.name)
+ else:
+ logger.debug("Failed to generate subfolder name for file: %s", rpd_file.name)
+
# Check for any errors
if not rpd_file.download_subfolder or not rpd_file.download_name:
if not rpd_file.download_subfolder and not rpd_file.download_name:
@@ -360,59 +402,93 @@ class SubfolderFile(multiprocessing.Process):
rpd_file.add_extra_detail(pn.NO_DATA_TO_NAME, {'filetype': area})
generation_succeeded = False
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}
-
-
+
+
if generation_succeeded:
rpd_file.download_path = os.path.join(rpd_file.download_folder, rpd_file.download_subfolder)
rpd_file.download_full_file_name = os.path.join(rpd_file.download_path, rpd_file.download_name)
rpd_file.download_full_base_name = os.path.splitext(rpd_file.download_full_file_name)[0]
-
+
subfolder = gio.File(path=rpd_file.download_path)
-
+
# Create subfolder if it does not exist.
# It is possible to skip the query step, and just try to create
# the directories and ignore the error of it already existing -
# but it takes twice as long to fail with an error than just
# run the straight query
-
+
+ logger.debug("Probing to see if subfolder already exists...")
if not subfolder.query_exists(cancellable=None):
- try:
- subfolder.make_directory_with_parents(cancellable=gio.Cancellable())
- except gio.Error, inst:
- # The directory may have been created by another process
- # between the time it takes to query and the time it takes
- # to create a new directory. Ignore such errors.
- if inst.code <> gio.ERROR_EXISTS:
- logger.error("Failed to create download subfolder: %s", rpd_file.download_path)
- logger.error(inst)
- rpd_file.error_title = _("Failed to create download subfolder")
- rpd_file.error_msg = _("Path: %s") % rpd_file.download_path
-
+ if self.use_gnome_file_operations:
+ try:
+ logger.debug("...subfolder doesn't exist: creating it using gnome...")
+ subfolder.make_directory_with_parents(cancellable=gio.Cancellable())
+ logger.debug("...subfolder created")
+ except gio.Error, inst:
+ # The directory may have been created by another process
+ # between the time it takes to query and the time it takes
+ # to create a new directory. Ignore such errors.
+ if inst.code <> gio.ERROR_EXISTS:
+ logger.error("Failed to create download subfolder: %s", rpd_file.download_path)
+ logger.error(inst)
+ rpd_file.error_title = _("Failed to create download subfolder")
+ rpd_file.error_msg = _("Path: %s") % rpd_file.download_path
+ else:
+ try:
+ logger.debug("...subfolder doesn't exist: creating it using python...")
+ os.makedirs(rpd_file.download_path)
+ logger.debug("...subfolder created")
+ except IOError as inst:
+ if inst.errno <> errno.EEXIST:
+ logger.error("Failed to create download subfolder: %s", rpd_file.download_path)
+ logger.error(inst)
+ rpd_file.error_title = _("Failed to create download subfolder")
+ rpd_file.error_msg = _("Path: %s") % rpd_file.download_path
+ else:
+ logger.debug("...subfolder already exists")
+
# Move temp file to subfolder
- download_file = gio.File(rpd_file.download_full_file_name)
-
add_unique_identifier = False
- try:
- temp_file.move(download_file, self.progress_callback_no_update, cancellable=None)
- move_succeeded = True
- if rpd_file.status <> config.STATUS_DOWNLOADED_WITH_WARNING:
- rpd_file.status = config.STATUS_DOWNLOADED
- except gio.Error, inst:
- if inst.code == gio.ERROR_EXISTS:
- if (rpd_file.download_conflict_resolution ==
- config.ADD_UNIQUE_IDENTIFIER):
- add_unique_identifier = True
+ if self.use_gnome_file_operations:
+ download_file = gio.File(rpd_file.download_full_file_name)
+ try:
+ logger.debug("Attempting to use Gnome to rename file %s to %s .....", rpd_file.temp_full_file_name, rpd_file.download_full_file_name)
+ temp_file.move(download_file, self.progress_callback_no_update, cancellable=None)
+ logger.debug("....successfully renamed file")
+ move_succeeded = True
+ if rpd_file.status <> config.STATUS_DOWNLOADED_WITH_WARNING:
+ rpd_file.status = config.STATUS_DOWNLOADED
+ except gio.Error as inst:
+ if inst.code == gio.ERROR_EXISTS:
+ rpd_file, add_unique_identifier = download_file_exists(rpd_file)
else:
- rpd_file = self.file_exists(rpd_file)
- else:
- rpd_file = self.download_failure_file_error(rpd_file, inst)
-
+ rpd_file = self.download_failure_file_error(rpd_file, inst)
+ else:
+ # Use python library functions to rename file
+ # Sadly this code basically duplicates the logic of the previous block
+ try:
+ if os.path.exists(rpd_file.download_full_file_name):
+ raise IOError(errno.EEXIST, "File exists: %s" % rpd_file.download_full_file_name)
+ logger.debug("Attempting to use python to rename file %s to %s .....", rpd_file.temp_full_file_name, rpd_file.download_full_file_name)
+ shutil.move(rpd_file.temp_full_file_name, rpd_file.download_full_file_name)
+ logger.debug("....successfully renamed file")
+ move_succeeded = True
+ if rpd_file.status <> config.STATUS_DOWNLOADED_WITH_WARNING:
+ rpd_file.status = config.STATUS_DOWNLOADED
+ except IOError as inst:
+ if inst.errno == errno.EEXIST:
+ rpd_file, add_unique_identifier = self.download_file_exists(rpd_file)
+ else:
+ rpd_file = self.download_failure_file_error(rpd_file, inst.strerror)
+ except:
+ rpd_file = self.download_failure_file_error(rpd_file, inst.strerror)
+
if add_unique_identifier:
name = os.path.splitext(rpd_file.download_name)
full_name = rpd_file.download_full_file_name
@@ -425,25 +501,35 @@ class SubfolderFile(multiprocessing.Process):
rpd_file.download_full_file_name = os.path.join(
rpd_file.download_path,
rpd_file.download_name)
- download_file = gio.File(
+
+ if self.use_gnome_file_operations:
+ download_file = gio.File(
rpd_file.download_full_file_name)
-
- try:
- temp_file.move(download_file, self.progress_callback_no_update, cancellable=None)
- move_succeeded = True
- suffix_already_used = False
- rpd_file = self.file_exists(rpd_file, identifier)
- logger.error("%s: %s - %s", rpd_file.full_file_name,
- rpd_file.problem.get_title(),
- rpd_file.problem.get_problems())
- except gio.Error, inst:
- if inst.code <> gio.ERROR_EXISTS:
+ try:
+ temp_file.move(download_file, self.progress_callback_no_update, cancellable=None)
+ rpd_file, move_succeeded, suffix_already_used = self.added_unique_identifier(rpd_file)
+ except gio.Error, inst:
+ if inst.code <> gio.ERROR_EXISTS:
+ rpd_file = self.download_failure_file_error(rpd_file, inst)
+ else:
+ try:
+ if os.path.exists(rpd_file.download_full_file_name):
+ raise IOError(errno.EEXIST, "File exists: %s" % rpd_file.download_full_file_name)
+ shutil.move(rpd_file.temp_full_file_name, rpd_file.download_full_file_name)
+ rpd_file, move_succeeded, suffix_already_used = self.added_unique_identifier(rpd_file)
+ except IOError as inst:
+ if inst.errno <> errno.EEXIST:
+ rpd_file = self.download_failure_file_error(rpd_file, inst)
+ break
+ except:
rpd_file = self.download_failure_file_error(rpd_file, inst)
-
-
-
- logger.debug("Finish processing file: %s", download_count)
-
+ break
+
+
+
+
+ logger.debug("Finish processing file: %s", download_count)
+
if move_succeeded:
if synchronize_raw_jpg:
if sequence_to_use is None:
@@ -458,17 +544,15 @@ class SubfolderFile(multiprocessing.Process):
if sequence_to_use is None:
if self.uses_session_sequece_no.value or self.uses_sequence_letter.value:
self.sequences.increment(
- self.uses_session_sequece_no.value,
+ self.uses_session_sequece_no.value,
self.uses_sequence_letter.value)
if self.uses_stored_sequence_no.value:
self.stored_sequence_no.value += 1
self.downloads_today_tracker.increment_downloads_today()
self.downloads_today.value = self.downloads_today_tracker.get_raw_downloads_today()
self.downloads_today_date.value = self.downloads_today_tracker.get_raw_downloads_today_date()
-
+
if rpd_file.temp_thm_full_name:
- # copy and rename THM video file
- source = gio.File(path=rpd_file.temp_thm_full_name)
ext = None
if hasattr(rpd_file, 'thm_extension'):
if rpd_file.thm_extension:
@@ -476,45 +560,69 @@ class SubfolderFile(multiprocessing.Process):
if ext is None:
ext = '.THM'
download_thm_full_name = rpd_file.download_full_base_name + ext
- 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)
-
+
+ # 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)
+
if rpd_file.temp_xmp_full_name:
# copy and rename XMP sidecar file
- source = gio.File(path=rpd_file.temp_xmp_full_name)
# generate_name() has generated xmp extension with correct capitalization
download_xmp_full_name = rpd_file.download_full_base_name + rpd_file.xmp_extension
- dest = gio.File(path=download_xmp_full_name)
- try:
- source.move(dest, self.progress_callback_no_update, cancellable=None)
- rpd_file.download_xmp_full_name = download_xmp_full_name
- except gio.Error, inst:
- logger.error("Failed to move XMP sidecar file %s", download_xmp_full_name)
-
-
+ if self.use_gnome_file_operations:
+ source = gio.File(path=rpd_file.temp_xmp_full_name)
+ dest = gio.File(path=download_xmp_full_name)
+ try:
+ source.move(dest, self.progress_callback_no_update, cancellable=None)
+ rpd_file.download_xmp_full_name = download_xmp_full_name
+ except gio.Error, inst:
+ logger.error("Failed to move XMP sidecar file %s", download_xmp_full_name)
+ else:
+ try:
+ shutil.move(rpd_file.temp_xmp_full_name, download_xmp_full_name)
+ rpd_file.download_xmp_full_name = download_xmp_full_name
+ except:
+ logger.error("Failed to move XMP sidecar file %s", download_xmp_full_name)
+
+
if not move_succeeded:
- logger.error("%s: %s - %s", rpd_file.full_file_name,
- rpd_file.problem.get_title(),
+ logger.error("%s: %s - %s", rpd_file.full_file_name,
+ rpd_file.problem.get_title(),
rpd_file.problem.get_problems())
- try:
- temp_file.delete(cancellable=None)
- except gio.Error, inst:
- logger.error("Failed to delete temporary file %s", rpd_file.temp_full_file_name)
- logger.error(inst)
-
-
-
-
-
+ if self.use_gnome_file_operations:
+ try:
+ temp_file.delete(cancellable=None)
+ except gio.Error, inst:
+ logger.error("Failed to delete temporary file %s", rpd_file.temp_full_file_name)
+ logger.error(inst)
+ else:
+ try:
+ os.remove(rpd_file.temp_full_file_name)
+ except:
+ logger.error("Failed to delete temporary file %s", rpd_file.temp_full_file_name)
+
+
+
+
+
rpd_file.metadata = None #purge metadata, as it cannot be pickled
rpd_file.sequences = None
- self.results_pipe.send((move_succeeded, rpd_file,))
-
+ self.results_pipe.send((move_succeeded, rpd_file, download_count))
+
i += 1
-
+