From 6866d4a5b74779f087b8e44148a49163d8b7327b Mon Sep 17 00:00:00 2001 From: Julien Valroff Date: Fri, 25 Mar 2011 22:07:00 +0100 Subject: Imported Upstream version 0.4.0~alpha1 --- PKG-INFO | 2 +- po/rapid-photo-downloader.pot | 1836 ++++---- rapid-photo-downloader | 2 +- rapid/ChangeLog | 42 +- rapid/common.py | 225 - rapid/config.py | 18 +- rapid/copyfiles.py | 170 + rapid/device.py | 176 + rapid/dropshadow.py | 183 - rapid/filmstrip.py | 2 +- rapid/generatename.py | 445 ++ rapid/generatenameconfig.py | 482 +++ rapid/glade3/about.ui | 75 + rapid/glade3/photo.png | Bin 17234 -> 0 bytes rapid/glade3/photo.svg | 1208 ++++++ rapid/glade3/photo24.png | Bin 528 -> 0 bytes rapid/glade3/photo66.png | Bin 0 -> 5738 bytes rapid/glade3/photo_icon.png | Bin 0 -> 464 bytes rapid/glade3/photo_small.png | Bin 924 -> 0 bytes rapid/glade3/photo_small_shadow.png | Bin 1711 -> 0 bytes rapid/glade3/prefs.ui | 3284 ++++++++++++++ rapid/glade3/rapid.glade | 3559 --------------- rapid/glade3/rapid.ui | 990 +++++ rapid/glade3/thumbnails_icon.png | Bin 0 -> 582 bytes rapid/glade3/video.png | Bin 13148 -> 0 bytes rapid/glade3/video.svg | 956 +++++ rapid/glade3/video24.png | Bin 736 -> 0 bytes rapid/glade3/video66.png | Bin 0 -> 4646 bytes rapid/glade3/video_small.png | Bin 1629 -> 0 bytes rapid/glade3/video_small_shadow.png | Bin 2556 -> 0 bytes rapid/gnomeglade.py | 166 - rapid/idletube.py | 205 - rapid/media.py | 339 -- rapid/metadata.py | 587 --- rapid/metadataphoto.py | 426 ++ rapid/metadatavideo.py | 220 + rapid/preferencesdialog.py | 1583 +++++++ rapid/prefsrapid.py | 395 ++ rapid/problemnotification.py | 4 +- rapid/rapid.py | 8110 +++++++++-------------------------- rapid/renamesubfolderprefs.py | 1693 -------- rapid/rpdfile.py | 277 ++ rapid/rpdmultiprocessing.py | 25 + rapid/scan.py | 170 + rapid/subfolderfile.py | 259 ++ rapid/tableplusminus.py | 168 +- rapid/thumbnail.py | 393 ++ rapid/utilities.py | 150 + rapid/videometadata.py | 250 -- setup.py | 18 +- 50 files changed, 14631 insertions(+), 14462 deletions(-) delete mode 100644 rapid/common.py create mode 100644 rapid/copyfiles.py create mode 100644 rapid/device.py delete mode 100755 rapid/dropshadow.py create mode 100644 rapid/generatename.py create mode 100644 rapid/generatenameconfig.py create mode 100644 rapid/glade3/about.ui delete mode 100644 rapid/glade3/photo.png create mode 100644 rapid/glade3/photo.svg delete mode 100644 rapid/glade3/photo24.png create mode 100644 rapid/glade3/photo66.png create mode 100644 rapid/glade3/photo_icon.png delete mode 100644 rapid/glade3/photo_small.png delete mode 100644 rapid/glade3/photo_small_shadow.png create mode 100644 rapid/glade3/prefs.ui delete mode 100644 rapid/glade3/rapid.glade create mode 100644 rapid/glade3/rapid.ui create mode 100644 rapid/glade3/thumbnails_icon.png delete mode 100644 rapid/glade3/video.png create mode 100644 rapid/glade3/video.svg delete mode 100644 rapid/glade3/video24.png create mode 100644 rapid/glade3/video66.png delete mode 100644 rapid/glade3/video_small.png delete mode 100644 rapid/glade3/video_small_shadow.png delete mode 100644 rapid/gnomeglade.py delete mode 100644 rapid/idletube.py delete mode 100755 rapid/media.py delete mode 100755 rapid/metadata.py create mode 100755 rapid/metadataphoto.py create mode 100644 rapid/metadatavideo.py create mode 100644 rapid/preferencesdialog.py create mode 100644 rapid/prefsrapid.py delete mode 100644 rapid/renamesubfolderprefs.py create mode 100644 rapid/rpdfile.py create mode 100644 rapid/rpdmultiprocessing.py create mode 100755 rapid/scan.py create mode 100644 rapid/subfolderfile.py create mode 100644 rapid/thumbnail.py create mode 100644 rapid/utilities.py delete mode 100755 rapid/videometadata.py diff --git a/PKG-INFO b/PKG-INFO index 298f6d4..3f8d230 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: rapid-photo-downloader -Version: 0.3.5 +Version: 0.4.0~a1 Summary: Rapid Photo Downloader for Linux Home-page: http://www.damonlynch.net/rapid Author: Damon Lynch diff --git a/po/rapid-photo-downloader.pot b/po/rapid-photo-downloader.pot index 5f1daf6..7abe954 100644 --- a/po/rapid-photo-downloader.pot +++ b/po/rapid-photo-downloader.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-12-28 02:49-0600\n" +"POT-Creation-Date: 2011-03-20 18:23-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,1608 +18,1321 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" #. Translators: if neccessary, for guidance in how to translate this program, you may see http://damonlynch.net/translate.html -#: rapid/rapid.py:124 rapid/rapid.py:6468 rapid/glade3/rapid.glade.h:71 +#: rapid/rapid.py:97 rapid/glade3/rapid.ui.h:11 msgid "Rapid Photo Downloader" msgstr "" -#: rapid/rapid.py:461 -msgid "New York" -msgstr "" - -#: rapid/rapid.py:462 -msgid "Manila" -msgstr "" - -#: rapid/rapid.py:462 -msgid "Prague" -msgstr "" - -#: rapid/rapid.py:462 -msgid "Helsinki" -msgstr "" - -#: rapid/rapid.py:462 -msgid "Wellington" -msgstr "" - -#: rapid/rapid.py:463 -msgid "Tehran" -msgstr "" - -#: rapid/rapid.py:463 -msgid "Kampala" -msgstr "" - -#: rapid/rapid.py:463 -msgid "Paris" -msgstr "" - -#: rapid/rapid.py:463 -msgid "Berlin" -msgstr "" - -#: rapid/rapid.py:463 -msgid "Sydney" -msgstr "" - -#: rapid/rapid.py:464 -msgid "Budapest" -msgstr "" - -#: rapid/rapid.py:464 -msgid "Rome" -msgstr "" - -#: rapid/rapid.py:464 -msgid "Moscow" -msgstr "" - -#: rapid/rapid.py:464 -msgid "Delhi" -msgstr "" - -#: rapid/rapid.py:464 -msgid "Warsaw" -msgstr "" - -#: rapid/rapid.py:465 -msgid "Jakarta" -msgstr "" - -#: rapid/rapid.py:465 -msgid "Madrid" -msgstr "" - -#: rapid/rapid.py:465 -msgid "Stockholm" -msgstr "" - -#: rapid/rapid.py:499 -msgid "Invalid Downloads Today value.\n" -msgstr "" - -#: rapid/rapid.py:500 -msgid "Resetting value to zero.\n" -msgstr "" - -#: rapid/rapid.py:539 -msgid "'Start of day' preference value is corrupted.\n" -msgstr "" - -#: rapid/rapid.py:540 -msgid "Resetting to midnight.\n" -msgstr "" - -#: rapid/rapid.py:566 -msgid "Error in Photo Rename preferences" -msgstr "" - -#: rapid/rapid.py:598 rapid/rapid.py:1720 -msgid "Sorry,these preferences contain an error:\n" -msgstr "" - -#: rapid/rapid.py:609 -msgid "Resetting to default values." -msgstr "" - -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode -#: rapid/rapid.py:684 rapid/renamesubfolderprefs.py:198 -msgid "Job code" -msgstr "" - -#: rapid/rapid.py:752 -msgid "Error in Video Rename preferences" -msgstr "" - -#: rapid/rapid.py:770 -msgid "Error in Photo Download Subfolders preferences" -msgstr "" - -#: rapid/rapid.py:787 -msgid "Error in Video Download Subfolders preferences" -msgstr "" - -#: rapid/rapid.py:820 rapid/rapid.py:1551 -msgid "photos and videos" -msgstr "" - -#: rapid/rapid.py:822 rapid/rapid.py:1561 rapid/rapid.py:1689 -msgid "photos" -msgstr "" - -#: rapid/rapid.py:955 -msgid "Select a folder to download photos to" -msgstr "" - -#: rapid/rapid.py:973 -msgid "Select a folder to download videos to" -msgstr "" - -#: rapid/rapid.py:1055 -#, python-format -msgid "Select a folder containing %(file_types)s" -msgstr "" - -#: rapid/rapid.py:1077 -#, python-format -msgid "Select a folder in which to backup %(file_types)s" -msgstr "" - -#. Translators: please do not modify or leave out html formatting tags like and . These are used to format the text the users sees -#: rapid/rapid.py:1158 -msgid "" -"Warning: There is insufficient metadata to fully generate the " -"name. Please use other renaming options." -msgstr "" - -#: rapid/rapid.py:1194 -msgid "" -"Warning: There is insufficient metadata to fully generate " -"subfolders. Please use other subfolder naming options." -msgstr "" - -#. Translators: you should not modify or leave out the %s. This is a code used by the programming language python to insert a value that thes user will see -#: rapid/rapid.py:1198 -#, python-format -msgid "Example: %s" -msgstr "" - -#: rapid/rapid.py:1224 -msgid "Downloads today value not updated, as a download is currently occurring" -msgstr "" - -#: rapid/rapid.py:1240 -msgid "Stored number value not updated, as a download is currently occurring" -msgstr "" - -#: rapid/rapid.py:1270 -#, python-format -msgid "" -"The %(filetype)s subfolder preferences had some unnecessary values removed." -msgstr "" - -#. Preferences list is now empty -#: rapid/rapid.py:1275 -#, python-format -msgid "" -"The %(filetype)s subfolder preferences entered are invalid and cannot be " -"used.\n" -"They will be reset to their default values." -msgstr "" - -#. check subfolder preferences for bad values -#: rapid/rapid.py:1289 rapid/rapid.py:1563 -msgid "photo" -msgstr "" - -#: rapid/rapid.py:1290 rapid/rapid.py:1558 -msgid "video" -msgstr "" - -#. Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. -#: rapid/rapid.py:1527 -msgid "externaldrive1" -msgstr "" - -#. Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. -#: rapid/rapid.py:1529 -msgid "externaldrive2" -msgstr "" - -#: rapid/rapid.py:1553 rapid/rapid.py:1687 -msgid "photos or videos" -msgstr "" - -#: rapid/rapid.py:1556 -msgid "videos" -msgstr "" - -#: rapid/rapid.py:1568 +#: rapid/rapid.py:102 #, python-format msgid "" "%(date)s\n" "%(time)s" msgstr "" -#: rapid/rapid.py:1570 +#: rapid/rapid.py:104 #, python-format msgid "%(date)s %(time)s" msgstr "" -#: rapid/rapid.py:1573 +#: rapid/rapid.py:107 #, python-format msgid "%(hour)s:%(minute)s:%(second)s:%(subsecond)s" msgstr "" -#: rapid/rapid.py:1580 +#: rapid/rapid.py:114 #, python-format msgid "%(date)s %(hour)s:%(minute)s:%(second)s:%(subsecond)s" msgstr "" -#: rapid/rapid.py:1608 rapid/rapid.py:2280 -msgid "subfolder and filename" +#. Device refers to a thing like a camera, memory card in its reader, +#. external hard drive, Portable Storage Device, etc. +#: rapid/rapid.py:145 +msgid "Device" msgstr "" -#: rapid/rapid.py:1610 rapid/rapid.py:2282 -msgid "filename" +#. Size refers to the total size of images on the device, typically in +#. MB or GB +#: rapid/rapid.py:159 +msgid "Size" msgstr "" -#: rapid/rapid.py:1612 rapid/rapid.py:2284 -msgid "subfolder" +#: rapid/rapid.py:162 +msgid "Download Progress" msgstr "" #. This refers to when a device like a hard drive is having its contents scanned, #. looking for photos or videos. It is visible initially in the progress bar for each device #. (which normally holds "x photos and videos"). #. It maybe displayed only briefly if the contents of the device being scanned is small. -#: rapid/rapid.py:1703 +#: rapid/rapid.py:938 msgid "scanning..." msgstr "" -#: rapid/rapid.py:1818 -msgid "The following download path could not be created:\n" +#: rapid/rapid.py:1639 +msgid "Download" msgstr "" -#: rapid/rapid.py:1819 -#, python-format -msgid "%(path)s: " -msgstr "" - -#: rapid/rapid.py:1820 rapid/rapid.py:1821 rapid/rapid.py:1836 -#: rapid/rapid.py:1837 rapid/rapid.py:2097 rapid/rapid.py:2100 -msgid "Download cannot proceed" -msgstr "" - -#: rapid/rapid.py:1834 rapid/rapid.py:5105 -msgid "There is an error in the program preferences." -msgstr "" - -#: rapid/rapid.py:1835 -msgid "" -"\n" -"Please check preferences, restart the program, and try again." +#: rapid/rapid.py:1642 +msgid "Pause" msgstr "" -#. Translators: as already, mentioned the %s value should not be modified or left out. It may be moved if necessary. -#. It refers to the actual number of photos that can be copied. For example, the user might see the following: -#. '0 of 512 photos' or '0 of 10 videos' or '0 of 202 photos and videos'. -#. This particular text is displayed to the user before the download has started. -#: rapid/rapid.py:2008 +#. ~ scan_pid = rpd_file.scan_pid +#. ~ unique_id = rpd_file.unique_id +#: rapid/rapid.py:1906 #, python-format -msgid "%(number)s %(filetypes)s" +msgid "%(number)s of %(total)s %(filetypes)s" msgstr "" -#. Translators: as you have already seen, the text can contain values that should not be modified or left out by you, for example %s. -#. This text is another example of that, but it is is a little more complex. Here there are two values which will be displayed -#. to the user when they run the program, signifying the number of photos found, and the device they were found on. -#. %(number)s should be left exactly as is: 'number' should not be translated. The same applies to %(device)s: 'device' should -#. not be translated. Generally speaking, if translating the sentence requires it, you can move items like '%(xyz)s' around -#. in a sentence, but you should never modify them or leave them out. -#: rapid/rapid.py:2019 -#, python-format -msgid "Device scan complete: found %(number)s %(filetypes)s on %(device)s" +#: rapid/rapid.py:2239 rapid/preferencesdialog.py:817 rapid/rpdfile.py:118 +msgid "photos" msgstr "" -#: rapid/rapid.py:2026 -#, python-format -msgid "Device scan complete: no %(filetypes)s found on %(device)s" +#: rapid/rapid.py:2241 rapid/rpdfile.py:113 +msgid "videos" msgstr "" -#. This message informs the user that the device (e.g. camera, hard drive or memory card) was automatically unmounted and they can now remove it -#: rapid/rapid.py:2047 -msgid "The device can now be safely removed" +#: rapid/rapid.py:2244 +msgid "Free space:" msgstr "" -#: rapid/rapid.py:2053 +#. (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 +#: rapid/rapid.py:2256 #, python-format -msgid "%(noFiles)s %(filetypes)s downloaded" +msgid "(%(file_type)s)" msgstr "" -#: rapid/rapid.py:2056 +#. Freespace available on the filesystem for downloading to +#. Displayed in status bar message on main window +#: rapid/rapid.py:2260 #, python-format -msgid "%(noFiles)s %(filetypes)s failed to download" -msgstr "" - -#: rapid/rapid.py:2059 rapid/rapid.py:5901 -msgid "warnings" +msgid "%(free)s %(file_type)s" msgstr "" -#: rapid/rapid.py:2061 rapid/rapid.py:5905 -msgid "errors" +#. Inserted in the middle of the statusbar message concerning the amount of freespace +#. Used to differentiate between two different file systems +#. e.g. Free space: 21.3GB (photos); 14.7GB (videos). +#: rapid/rapid.py:2265 +msgid "; " msgstr "" -#: rapid/rapid.py:2091 -#, python-format -msgid "Source: %s\n" +#. 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). +#: rapid/rapid.py:2270 +msgid "." msgstr "" -#: rapid/rapid.py:2093 +#. Freespace available on the filesystem for downloading to +#. Displayed in status bar message on main window +#. e.g. 14.7GB available +#: rapid/rapid.py:2276 #, python-format -msgid "Device: %s\n" +msgid "%(free)s free" msgstr "" -#: rapid/rapid.py:2094 +#. user manually specified backup location +#: rapid/rapid.py:2282 #, python-format -msgid "Destination: %s" -msgstr "" - -#: rapid/rapid.py:2095 rapid/rapid.py:2098 -msgid "Could not create temporary download directory" -msgstr "" - -#: rapid/rapid.py:2098 -msgid "Error:" -msgstr "" - -#: rapid/rapid.py:2122 -msgid "Backup path does not exist" +msgid "Backing up to %(path)s" msgstr "" -#: rapid/rapid.py:2123 +#: rapid/rapid.py:2287 #, python-format -msgid "The path %s could not be created" -msgstr "" - -#: rapid/rapid.py:2124 -msgid "No backups can occur" +msgid "%(freespace)s. %(backuppaths)s." msgstr "" -#: rapid/rapid.py:2153 -#, python-format -msgid "" -"Source: %(source)s\n" -"Destination: %(destination)s\n" -"%(problem)s" +#: rapid/rapid.py:2470 +msgid "Thumbnails" msgstr "" -#: rapid/rapid.py:2184 rapid/rapid.py:2530 rapid/rapid.py:2565 -#: rapid/rapid.py:2593 rapid/rapid.py:2624 rapid/rapid.py:2643 +#. 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. +#: rapid/rapid.py:2511 #, python-format msgid "" -"Source: %(source)s\n" -"Destination: %(destination)s" +"display program information on the command line as the program runs " +"(default: %default)" msgstr "" -#. hopefully inst will never be None, but just to be safe... -#: rapid/rapid.py:2199 -msgid "Please check your system and try again." +#: rapid/rapid.py:2512 +msgid "display debugging information when run from the command line" msgstr "" -#: rapid/rapid.py:2219 -msgid "Photos detected with the same filenames, but taken at different times" +#: rapid/rapid.py:2513 +msgid "only output errors to the command line" msgstr "" -#: rapid/rapid.py:2259 -msgid "Photo has already been downloaded" +#. image file extensions are recognized RAW files plus TIFF and JPG +#: rapid/rapid.py:2515 +msgid "list photo and video file extensions the program recognizes and exit" msgstr "" -#: rapid/rapid.py:2260 rapid/rapid.py:2659 -#, python-format -msgid "Source: %(source)s" +#: rapid/rapid.py:2516 +msgid "reset all program settings and preferences and exit" msgstr "" -#. A new day, according the user's preferences of what time a day begins, has started -#: rapid/rapid.py:2458 -msgid "New day has started - resetting 'Downloads Today' sequence number" +#: rapid/rapid.py:2529 +msgid "Photos:" msgstr "" #: rapid/rapid.py:2529 -#, python-format -msgid "Backup of %(file_type)s already exists" +msgid "Videos:" msgstr "" -#: rapid/rapid.py:2533 +#: rapid/rapid.py:2534 #, python-format -msgid "Backup %(file_type)s overwritten" +msgid "and %s" msgstr "" -#: rapid/rapid.py:2537 -#, python-format -msgid "%(file_type)s not backed up to %(volume)s" +#: rapid/rapid.py:2542 +msgid "All settings and preferences have been reset" msgstr "" -#: rapid/rapid.py:2539 -#, python-format -msgid "%(file_type)s not backed up" +#: rapid/rapid.py:2551 +msgid "" +"Video downloading functionality disabled.\n" +"To download videos, please install the hachoir metadata and kaa metadata " +"packages for python." msgstr "" -#: rapid/rapid.py:2562 rapid/rapid.py:2590 rapid/rapid.py:2623 -#: rapid/rapid.py:2642 -msgid "Backing up error" +#: rapid/device.py:79 +msgid "Device Detected" msgstr "" -#: rapid/rapid.py:2563 rapid/rapid.py:2591 -#, python-format -msgid "Destination directory could not be created: %(directory)s\n" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt +#: rapid/device.py:88 +msgid "" +"Should this device or partition be used to download photos or videos from?" msgstr "" -#: rapid/rapid.py:2567 rapid/rapid.py:2626 -#, python-format -msgid "Error: %(inst)s" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt +#: rapid/device.py:107 +msgid "_Remember this choice" msgstr "" -#: rapid/rapid.py:2568 rapid/rapid.py:2596 rapid/rapid.py:2627 -#: rapid/rapid.py:2646 -#, python-format -msgid "The %(file_type)s was not backed up." +#: rapid/generatenameconfig.py:143 +msgid "Date time" msgstr "" -#: rapid/rapid.py:2595 rapid/rapid.py:2645 -#, python-format -msgid "Error: %(errno)s %(strerror)s" +#: rapid/generatenameconfig.py:144 +msgid "Text" msgstr "" -#: rapid/rapid.py:2658 -#, python-format -msgid "%(file_type)s could not be backed up" +#: rapid/generatenameconfig.py:145 +msgid "Filename" msgstr "" -#: rapid/rapid.py:2661 -msgid "No suitable backup volume was found" +#: rapid/generatenameconfig.py:146 +msgid "Metadata" msgstr "" -#: rapid/rapid.py:2663 -msgid "A backup location was not found" +#: rapid/generatenameconfig.py:147 +msgid "Sequences" msgstr "" -#: rapid/rapid.py:2717 -#, python-format -msgid "This device has no %(types_searched_for)s to download from." +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode +#: rapid/generatenameconfig.py:149 rapid/preferencesdialog.py:545 +msgid "Job code" msgstr "" -#: rapid/rapid.py:2813 -#, python-format -msgid "Download has started from %s" +#: rapid/generatenameconfig.py:150 +msgid "Image date" msgstr "" -#: rapid/rapid.py:2817 -#, python-format -msgid "Attempting to download %s files" +#: rapid/generatenameconfig.py:151 +msgid "Video date" msgstr "" -#. reset the progress bar to update the status of this download attempt -#: rapid/rapid.py:2849 rapid/rapid.py:2917 -#, python-format -msgid "%(number)s of %(total)s %(filetypes)s" +#: rapid/generatenameconfig.py:152 +msgid "Today" msgstr "" -#: rapid/rapid.py:2913 -#, python-format -msgid "%(number)s of %(total)s %(filetypes)s (%(remaining)s remaining)" +#: rapid/generatenameconfig.py:153 +msgid "Yesterday" msgstr "" -#: rapid/rapid.py:2938 rapid/rapid.py:2942 -msgid "Could not delete photo or video from device" +#. Translators: Download time is the time and date that the download started (when the user clicked the Download button) +#: rapid/generatenameconfig.py:155 +msgid "Download time" msgstr "" -#: rapid/rapid.py:2939 -#, python-format -msgid "" -"Photo: %(source)s\n" -"Error: %(errno)s %(strerror)s" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename +#: rapid/generatenameconfig.py:157 +msgid "Name + extension" msgstr "" -#: rapid/rapid.py:2943 -#, python-format -msgid "Photo: %(source)s" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename +#: rapid/generatenameconfig.py:159 +msgid "Name" msgstr "" -#: rapid/rapid.py:2945 -#, python-format -msgid "Deleted %(number)i %(filetypes)s from device" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename +#: rapid/generatenameconfig.py:161 +msgid "Extension" msgstr "" -#: rapid/rapid.py:2958 -#, python-format -msgid "Download complete from %s" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename +#: rapid/generatenameconfig.py:163 +msgid "Image number" msgstr "" -#. Device refers to a thing like a camera, memory card in its reader, external hard drive, Portable Storage Device, etc. -#: rapid/rapid.py:3073 rapid/rapid.py:3542 -msgid "Device" +#: rapid/generatenameconfig.py:164 +msgid "Video number" msgstr "" -#. Size refers to the total size of images on the device, typically in MB or GB -#: rapid/rapid.py:3078 rapid/rapid.py:3531 -msgid "Size" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata +#: rapid/generatenameconfig.py:166 +msgid "Aperture" msgstr "" -#: rapid/rapid.py:3081 -msgid "Download Progress" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata +#: rapid/generatenameconfig.py:168 +msgid "ISO" msgstr "" -#: rapid/rapid.py:3169 -msgid "Downloading From Cameras" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata +#: rapid/generatenameconfig.py:170 +msgid "Exposure time" msgstr "" -#: rapid/rapid.py:3175 -msgid "Downloading directly from a camera may work poorly or not at all" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata +#: rapid/generatenameconfig.py:172 +msgid "Focal length" msgstr "" -#: rapid/rapid.py:3176 -msgid "" -"Downloading from a card reader always works and is generally much faster. It " -"is strongly recommended to use a card reader." +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata +#: rapid/generatenameconfig.py:174 +msgid "Camera make" msgstr "" -#: rapid/rapid.py:3190 -msgid "_Show this message again" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata +#: rapid/generatenameconfig.py:176 +msgid "Camera model" msgstr "" -#: rapid/rapid.py:3225 -msgid "Device Detected" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata +#: rapid/generatenameconfig.py:178 +msgid "Short camera model" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt -#: rapid/rapid.py:3234 -msgid "" -"Should this device or partition be used to download photos or videos from?" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata +#: rapid/generatenameconfig.py:180 +msgid "Hyphenated short camera model" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt -#: rapid/rapid.py:3253 -msgid "_Remember this choice" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata +#: rapid/generatenameconfig.py:182 +msgid "Serial number" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt -#: rapid/rapid.py:3290 -#, python-format -msgid "%s selected for downloading from" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata +#: rapid/generatenameconfig.py:184 +msgid "Shutter count" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt -#: rapid/rapid.py:3293 -msgid "This device or partition will always be used to download from" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata +#: rapid/generatenameconfig.py:186 +msgid "Owner name" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt -#: rapid/rapid.py:3296 -#, python-format -msgid "%s rejected as a download device" +#: rapid/generatenameconfig.py:187 +msgid "Codec" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt -#: rapid/rapid.py:3299 -msgid "This device or partition will never be used to download from" +#: rapid/generatenameconfig.py:188 +msgid "Width" msgstr "" -#: rapid/rapid.py:3306 -msgid "Remove all Job Codes?" +#: rapid/generatenameconfig.py:189 +msgid "Height" msgstr "" -#: rapid/rapid.py:3323 -msgid "Should all Job Codes be removed?" +#: rapid/generatenameconfig.py:190 +msgid "Length" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode -#: rapid/rapid.py:3351 -msgid "Enter a Job Code" +#: rapid/generatenameconfig.py:191 +msgid "Frames Per Second" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode -#: rapid/rapid.py:3370 -msgid "Enter a new Job Code, or select a previous one" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers +#: rapid/generatenameconfig.py:193 +msgid "Downloads today" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode -#: rapid/rapid.py:3373 -msgid "Enter a new Job Code" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers +#: rapid/generatenameconfig.py:195 +msgid "Session number" msgstr "" -#: rapid/rapid.py:3378 rapid/rapid.py:4640 -msgid "Job Code:" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers +#: rapid/generatenameconfig.py:197 +msgid "Subfolder number" msgstr "" -#: rapid/rapid.py:3425 -msgid "Job Code entered" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers +#: rapid/generatenameconfig.py:199 +msgid "Stored number" msgstr "" -#: rapid/rapid.py:3427 -msgid "Job Code not entered" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequenceletters +#: rapid/generatenameconfig.py:201 +msgid "Sequence letter" msgstr "" -#: rapid/rapid.py:3477 -msgid "Status" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename +#: rapid/generatenameconfig.py:203 +msgid "All digits" msgstr "" -#: rapid/rapid.py:3485 -msgid "Type" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename +#: rapid/generatenameconfig.py:205 +msgid "Last digit" msgstr "" -#: rapid/rapid.py:3494 rapid/rapid.py:5067 rapid/rapid.py:5069 -msgid "Photo" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename +#: rapid/generatenameconfig.py:207 +msgid "Last 2 digits" msgstr "" -#: rapid/rapid.py:3496 -msgid "File" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename +#: rapid/generatenameconfig.py:209 +msgid "Last 3 digits" msgstr "" -#: rapid/rapid.py:3511 -msgid "Job Code" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename +#: rapid/generatenameconfig.py:211 +msgid "Last 4 digits" msgstr "" -#: rapid/rapid.py:3521 -msgid "Date" +#. Translators: please not the capitalization of this text, and keep it the same if your language features capitalization +#: rapid/generatenameconfig.py:213 +msgid "Original Case" msgstr "" -#: rapid/rapid.py:3553 rapid/renamesubfolderprefs.py:194 -msgid "Filename" +#. Translators: please not the capitalization of this text, and keep it the same if your language features capitalization +#: rapid/generatenameconfig.py:215 +msgid "UPPERCASE" msgstr "" -#: rapid/rapid.py:3564 -msgid "Path" +#. Translators: please not the capitalization of this text, and keep it the same if your language features capitalization +#: rapid/generatenameconfig.py:217 +msgid "lowercase" msgstr "" -#: rapid/rapid.py:3974 -#, python-format -msgid "%(filetype)s was downloaded successfully" +#: rapid/generatenameconfig.py:218 +msgid "One digit" msgstr "" -#: rapid/rapid.py:3976 -#, python-format -msgid "%(filetype)s was not downloaded" +#: rapid/generatenameconfig.py:219 +msgid "Two digits" msgstr "" -#: rapid/rapid.py:3978 -#, python-format -msgid "%(filetype)s was downloaded with warnings" +#: rapid/generatenameconfig.py:220 +msgid "Three digits" msgstr "" -#: rapid/rapid.py:3980 -#, python-format -msgid "%(filetype)s was downloaded but there were problems backing up" +#: rapid/generatenameconfig.py:221 +msgid "Four digits" msgstr "" -#: rapid/rapid.py:3982 -#, python-format -msgid "%(filetype)s was neither downloaded nor backed up" +#: rapid/generatenameconfig.py:222 +msgid "Five digits" msgstr "" -#: rapid/rapid.py:3984 -#, python-format -msgid "%(filetype)s is ready to be downloaded" +#: rapid/generatenameconfig.py:223 +msgid "Six digits" msgstr "" -#: rapid/rapid.py:3986 -#, python-format -msgid "%(filetype)s is about to be downloaded" +#: rapid/generatenameconfig.py:224 +msgid "Seven digits" msgstr "" -#: rapid/rapid.py:3988 -#, python-format -msgid "%(filetype)s will be downloaded with warnings" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:226 +msgid "Subseconds" msgstr "" -#: rapid/rapid.py:3990 -#, python-format -msgid "%(filetype)s cannot be downloaded" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:228 +msgid "YYYYMMDD" msgstr "" -#: rapid/rapid.py:4662 -msgid "Enter a new Job Code and press Enter, or select an existing Job Code" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:230 +msgid "YYYY-MM-DD" msgstr "" -#: rapid/rapid.py:5034 -#, python-format -msgid "%(free)s available" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:232 +msgid "YYMMDD" msgstr "" -#. user manually specified backup location -#: rapid/rapid.py:5040 -#, python-format -msgid "Backing up to %(path)s" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:234 +msgid "YY-MM-DD" msgstr "" -#: rapid/rapid.py:5045 -#, python-format -msgid "%(freespace)s. %(backuppaths)s." +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:236 +msgid "MMDDYYYY" msgstr "" -#: rapid/rapid.py:5054 -#, python-format -msgid "" -"Sorry, this device location does not exist:\n" -"%(path)s\n" -"\n" -"Please resolve the problem, or modify your preferences." +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:238 +msgid "MMDDYY" msgstr "" -#: rapid/rapid.py:5058 -msgid "Problem with Device Location Folder" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:240 +msgid "MMDD" msgstr "" -#: rapid/rapid.py:5067 -msgid "Video" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:242 +msgid "DDMMYYYY" msgstr "" -#: rapid/rapid.py:5074 -#, python-format -msgid "The %(file_type)s Download Folder does not exist.\n" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:244 +msgid "DDMMYY" msgstr "" -#: rapid/rapid.py:5083 -#, python-format -msgid "The %(file_type)s Download Folder exists but cannot be written to.\n" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:246 +msgid "YYYY" msgstr "" -#: rapid/rapid.py:5088 -msgid "" -"Sorry, problems were encountered with your download folders. Please fix the " -"problems or modify the preferences.\n" -"\n" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:248 +msgid "YY" msgstr "" -#: rapid/rapid.py:5091 -msgid "Problem with Download Folder" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:250 +msgid "MM" msgstr "" -#: rapid/rapid.py:5093 -msgid "Problem with Download Folders" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:252 +msgid "DD" msgstr "" -#: rapid/rapid.py:5106 -msgid "Some preferences will be reset." +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:254 +msgid "HHMMSS" msgstr "" -#: rapid/rapid.py:5139 -msgid "Displaying warning about downloading directly from camera" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:256 +msgid "HHMM" msgstr "" -#: rapid/rapid.py:5149 -#, python-format -msgid "Prompting whether to use %s" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:258 +msgid "HH-MM-SS" msgstr "" -#: rapid/rapid.py:5176 -msgid "Prompting for Job Code" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:260 +msgid "HH-MM" msgstr "" -#: rapid/rapid.py:5180 -msgid "Already prompting for Job Code, do not prompt again" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:262 +msgid "HH" msgstr "" -#: rapid/rapid.py:5197 -msgid "Starting downloads" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:264 +msgid "MM (minutes)" msgstr "" -#. autostart is true -#: rapid/rapid.py:5201 -msgid "Starting downloads that have been waiting for a Job Code" +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime +#: rapid/generatenameconfig.py:266 +msgid "SS" msgstr "" -#: rapid/rapid.py:5248 +#: rapid/preferencesdialog.py:364 #, python-format -msgid "Creating photo download folder %(folder)s" +msgid "Subfolder preferences should not start with a %s" msgstr "" -#: rapid/rapid.py:5253 +#: rapid/preferencesdialog.py:366 #, python-format -msgid "Failed to create default photo download folder %(folder)s" +msgid "Subfolder preferences should not end with a %s" msgstr "" -#: rapid/rapid.py:5257 +#: rapid/preferencesdialog.py:370 #, python-format -msgid "Creating video download folder %(folder)s" +msgid "Subfolder preferences should not contain two %s one after the other" msgstr "" -#: rapid/rapid.py:5262 -#, python-format -msgid "Failed to create default video download folder %(folder)s" +#: rapid/preferencesdialog.py:384 rapid/preferencesdialog.py:668 +msgid "Remove all Job Codes?" msgstr "" -#: rapid/rapid.py:5287 -msgid "" -"A newer version of this program was previously run on this computer.\n" -"\n" +#: rapid/preferencesdialog.py:401 rapid/preferencesdialog.py:685 +msgid "Should all Job Codes be removed?" msgstr "" -#: rapid/rapid.py:5289 -msgid "" -"Program preferences appear to be valid, but please check them to ensure " -"correct operation." +#: rapid/preferencesdialog.py:431 +msgid "Error in Photo Rename preferences" msgstr "" -#: rapid/rapid.py:5291 -msgid "Sorry, some preferences are invalid and will be reset." +#: rapid/preferencesdialog.py:613 +msgid "Error in Video Rename preferences" msgstr "" -#: rapid/rapid.py:5292 -msgid "Warning:" +#: rapid/preferencesdialog.py:634 +msgid "Error in Photo Download Subfolders preferences" msgstr "" -#: rapid/rapid.py:5297 -msgid "" -"This version of the program is newer than the previously run version. " -"Checking preferences." +#: rapid/preferencesdialog.py:651 +msgid "Error in Video Download Subfolders preferences" msgstr "" -#: rapid/rapid.py:5304 -msgid "Preferences were modified." +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode +#: rapid/preferencesdialog.py:710 +msgid "Enter a Job Code" msgstr "" -#: rapid/rapid.py:5305 -msgid "" -"This version of the program uses different preferences than the old version. " -"Your preferences have been updated.\n" -"\n" -"Please check them to ensure correct operation." +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode +#: rapid/preferencesdialog.py:727 +msgid "Enter a new Job Code, or select a previous one" msgstr "" -#: rapid/rapid.py:5309 -msgid "No preferences needed to be changed." +#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode +#: rapid/preferencesdialog.py:730 +msgid "Enter a new Job Code" msgstr "" -#: rapid/rapid.py:5311 -msgid "" -"This version of the program uses different preferences than the old version. " -"Some of your previous preferences were invalid, and could not be updated. " -"They will be reset." +#: rapid/preferencesdialog.py:735 +msgid "Job Code:" msgstr "" -#: rapid/rapid.py:5321 -msgid "Problem using pynotify." +#: rapid/preferencesdialog.py:815 rapid/rpdfile.py:108 +msgid "photos and videos" msgstr "" -#: rapid/rapid.py:5337 -msgid "Failed to receive pynotify server capabilities." +#: rapid/preferencesdialog.py:964 +msgid "Select a folder to download photos to" msgstr "" -#: rapid/rapid.py:5348 -msgid "" -"Warning: desktop environment notification server is incorrectly configured." +#: rapid/preferencesdialog.py:982 +msgid "Select a folder to download videos to" msgstr "" -#: rapid/rapid.py:5396 -msgid "and" +#: rapid/preferencesdialog.py:1064 +#, python-format +msgid "Select a folder containing %(file_types)s" msgstr "" -#: rapid/rapid.py:5401 -msgid "Using backup devices" +#: rapid/preferencesdialog.py:1086 +#, python-format +msgid "Select a folder in which to backup %(file_types)s" msgstr "" -#: rapid/rapid.py:5403 -msgid "Using backup device" +#. Translators: please do not modify or leave out html formatting tags like and . These are used to format the text the users sees +#: rapid/preferencesdialog.py:1172 +msgid "" +"Warning: There is insufficient metadata to fully generate the " +"name. Please use other renaming options." msgstr "" -#: rapid/rapid.py:5405 -msgid "No backup devices detected" +#: rapid/preferencesdialog.py:1211 +msgid "" +"Warning: There is insufficient metadata to fully generate " +"subfolders. Please use other subfolder naming options." msgstr "" -#: rapid/rapid.py:5455 rapid/rapid.py:5617 +#. Translators: you should not modify or leave out the %s. This is a code used by the programming language python to insert a value that thes user will see +#: rapid/preferencesdialog.py:1215 #, python-format -msgid "Device %(device)s (%(path)s) ignored" +msgid "Example: %s" msgstr "" -#: rapid/rapid.py:5576 +#. Preferences list is now empty +#: rapid/preferencesdialog.py:1291 #, python-format -msgid "Detected %(device)s with path %(path)s" +msgid "" +"The %(filetype)s subfolder preferences entered are invalid and cannot be " +"used.\n" +"They will be reset to their default values." msgstr "" -#: rapid/rapid.py:5580 -msgid "Automatically start download is true" +#. check subfolder preferences for bad values +#: rapid/preferencesdialog.py:1305 rapid/rpdfile.py:120 rapid/rpdfile.py:241 +msgid "photo" msgstr "" -#: rapid/rapid.py:5582 -msgid "Automatically start download is false" +#: rapid/preferencesdialog.py:1306 rapid/rpdfile.py:115 rapid/rpdfile.py:267 +msgid "video" msgstr "" -#: rapid/rapid.py:5635 -msgid "Using manually specified path" +#. Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. +#: rapid/preferencesdialog.py:1566 +msgid "externaldrive1" msgstr "" -#. the user is trying to backup to a device that is currently being downloaded from..... we don't normally allow that, but what to do? -#: rapid/rapid.py:5703 -#, python-format -msgid "Warning: backup device %(device)s is currently being downloaded from" +#. Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. +#: rapid/preferencesdialog.py:1568 +msgid "externaldrive2" msgstr "" -#: rapid/rapid.py:5719 -msgid "D_ownload Selected" +#: rapid/prefsrapid.py:122 +msgid "New York" msgstr "" -#: rapid/rapid.py:5822 rapid/rapid.py:5873 -msgid "All downloads complete" +#: rapid/prefsrapid.py:123 +msgid "Manila" msgstr "" -#: rapid/rapid.py:5837 -msgid "MB/s" +#: rapid/prefsrapid.py:123 +msgid "Prague" msgstr "" -#: rapid/rapid.py:5847 -msgid "About 1 second remaining" +#: rapid/prefsrapid.py:123 +msgid "Helsinki" msgstr "" -#: rapid/rapid.py:5849 -#, python-format -msgid "About %i seconds remaining" +#: rapid/prefsrapid.py:123 +msgid "Wellington" msgstr "" -#: rapid/rapid.py:5851 -msgid "About 1 minute remaining" +#: rapid/prefsrapid.py:124 +msgid "Tehran" msgstr "" -#. 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' -#: rapid/rapid.py:5856 -#, python-format -msgid "About %(minutes)i:%(seconds)02i minutes remaining" +#: rapid/prefsrapid.py:124 +msgid "Kampala" msgstr "" -#: rapid/rapid.py:5876 rapid/rapid.py:5882 rapid/rapid.py:5888 -#: rapid/rapid.py:5894 rapid/rapid.py:5899 rapid/rapid.py:5903 -#, python-format -msgid "%(number)s %(numberdownloaded)s" +#: rapid/prefsrapid.py:124 +msgid "Paris" msgstr "" -#: rapid/rapid.py:5878 rapid/rapid.py:5890 -#, python-format -msgid "%(filetype)s downloaded" +#: rapid/prefsrapid.py:124 +msgid "Berlin" msgstr "" -#: rapid/rapid.py:5884 rapid/rapid.py:5896 -#, python-format -msgid "%(filetype)s failed to download" +#: rapid/prefsrapid.py:124 +msgid "Sydney" msgstr "" -#: rapid/rapid.py:6070 -msgid "_Resume" +#: rapid/prefsrapid.py:125 +msgid "Budapest" msgstr "" -#: rapid/rapid.py:6073 -msgid "_Download All" +#: rapid/prefsrapid.py:125 +msgid "Rome" msgstr "" -#. This text will be displayed to the user on the Download / Pause button. -#: rapid/rapid.py:6082 -msgid "_Pause" +#: rapid/prefsrapid.py:125 +msgid "Moscow" msgstr "" -#: rapid/rapid.py:6194 -msgid "Download device settings preferences were changed." +#: rapid/prefsrapid.py:125 +msgid "Delhi" msgstr "" -#: rapid/rapid.py:6206 -msgid "Backup preferences were changed." +#: rapid/prefsrapid.py:125 +msgid "Warsaw" msgstr "" -#: rapid/rapid.py:6212 -msgid "Subfolder and filename preferences were changed." +#: rapid/prefsrapid.py:126 +msgid "Jakarta" msgstr "" -#: rapid/rapid.py:6223 -msgid "Download folder preferences were changed." +#: rapid/prefsrapid.py:126 +msgid "Madrid" msgstr "" -#: rapid/rapid.py:6422 -msgid "Goodbye" +#: rapid/prefsrapid.py:126 +msgid "Stockholm" msgstr "" -#. 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. -#: rapid/rapid.py:6433 -#, python-format -msgid "" -"display program information on the command line as the program runs " -"(default: %default)" +#. components +#: rapid/problemnotification.py:26 rapid/subfolderfile.py:181 +msgid "subfolder" msgstr "" -#: rapid/rapid.py:6434 -msgid "display debugging information when run from the command line" +#: rapid/problemnotification.py:27 rapid/subfolderfile.py:179 +msgid "filename" msgstr "" -#: rapid/rapid.py:6435 -msgid "only output errors to the command line" +#: rapid/problemnotification.py:80 +#, python-format +msgid "Date time value %s appears invalid." msgstr "" -#. image file extensions are recognized RAW files plus TIFF and JPG -#: rapid/rapid.py:6437 -msgid "list photo and video file extensions the program recognizes and exit" +#: rapid/problemnotification.py:81 +msgid "Filename does not have an extension." msgstr "" -#: rapid/rapid.py:6438 -msgid "reset all program settings and preferences and exit" +#. a number component is something like the 8346 in IMG_8346.JPG +#: rapid/problemnotification.py:83 +msgid "Filename does not have a number component." msgstr "" -#: rapid/rapid.py:6452 -msgid "Photos:" +#: rapid/problemnotification.py:84 +#, python-format +msgid "Error generating component %s." msgstr "" -#: rapid/rapid.py:6452 -msgid "Videos:" +#. a generic problem +#: rapid/problemnotification.py:86 +#, python-format +msgid "%(filetype)s metadata cannot be read" msgstr "" -#: rapid/rapid.py:6457 +#: rapid/problemnotification.py:88 #, python-format -msgid "and %s" +msgid "%(filetype)s %(area)s could not be generated" msgstr "" -#: rapid/rapid.py:6465 -msgid "All settings and preferences have been reset" +#: rapid/problemnotification.py:90 rapid/problemnotification.py:91 +#, python-format +msgid "An error occurred when copying the %(filetype)s" msgstr "" -#. Which volume management code is being used (GIO or GnomeVFS) -#: rapid/rapid.py:6469 rapid/rapid.py:6470 rapid/rapid.py:6472 -#: rapid/rapid.py:6477 rapid/rapid.py:6481 -msgid "Using" +#: rapid/problemnotification.py:93 rapid/problemnotification.py:94 +#, python-format +msgid "%(filetype)s already exists" msgstr "" -#: rapid/rapid.py:6474 -msgid "\n" +#: rapid/problemnotification.py:97 +#, python-format +msgid "" +"%(filetype)s could not be backed up because no suitable backup locations " +"were found." msgstr "" -#. this application is already running -#: rapid/rapid.py:6498 +#: rapid/problemnotification.py:102 #, python-format -msgid "%s is already running" +msgid "" +"%(image1)s was taken at on %(image1_date)s at %(image1_time)s, and " +"%(image2)s on %(image2_date)s at %(image2_time)s." msgstr "" -#: rapid/renamesubfolderprefs.py:192 -msgid "Date time" +#: rapid/problemnotification.py:103 +#, python-format +msgid "%(filetype)s was already downloaded" msgstr "" -#: rapid/renamesubfolderprefs.py:193 -msgid "Text" +#: rapid/problemnotification.py:107 +#, python-format +msgid "" +"The existing %(filetype)s was last modified on %(date)s at %(time)s. Unique " +"identifier '%(identifier)s' added." msgstr "" -#: rapid/renamesubfolderprefs.py:195 -msgid "Metadata" +#: rapid/problemnotification.py:108 +#, python-format +msgid "The existing %(filetype)s was last modified on %(date)s at %(time)s." msgstr "" -#: rapid/renamesubfolderprefs.py:196 -msgid "Sequences" +#: rapid/problemnotification.py:109 +#, python-format +msgid "There is no data with which to name the %(filetype)s." msgstr "" -#: rapid/renamesubfolderprefs.py:199 -msgid "Image date" +#: rapid/problemnotification.py:111 +#, python-format +msgid "Error: %(errorno)s %(strerror)s" msgstr "" -#: rapid/renamesubfolderprefs.py:200 -msgid "Video date" +#: rapid/problemnotification.py:201 +msgid "The metadata might be corrupt." msgstr "" -#: rapid/renamesubfolderprefs.py:201 -msgid "Today" +#: rapid/problemnotification.py:204 +msgid "" +"The filename, extension and Exif information indicate it has already been " +"downloaded." msgstr "" -#: rapid/renamesubfolderprefs.py:202 -msgid "Yesterday" +#: rapid/problemnotification.py:223 +#, python-format +msgid " It was backed up to %(volume)s" msgstr "" -#. Translators: Download time is the time and date that the download started (when the user clicked the Download button) -#: rapid/renamesubfolderprefs.py:204 -msgid "Download time" +#: rapid/problemnotification.py:225 +msgid " It was backed up to these devices: " msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename -#: rapid/renamesubfolderprefs.py:206 -msgid "Name + extension" +#: rapid/problemnotification.py:227 rapid/problemnotification.py:288 +#: rapid/problemnotification.py:300 +#, python-format +msgid "%s, " msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename -#: rapid/renamesubfolderprefs.py:208 -msgid "Name" +#: rapid/problemnotification.py:228 rapid/problemnotification.py:289 +#: rapid/problemnotification.py:301 +#, python-format +msgid "%(volumes)s and %(final_volume)s." msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename -#: rapid/renamesubfolderprefs.py:210 -msgid "Extension" +#: rapid/problemnotification.py:240 +#, python-format +msgid "" +"Photos detected with the same filenames, but taken at different times: " +"%(details)s" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename -#: rapid/renamesubfolderprefs.py:212 -msgid "Image number" +#: rapid/problemnotification.py:257 +#, python-format +msgid "An error occurred when backing up on %(volume)s: %(inst)s." msgstr "" -#: rapid/renamesubfolderprefs.py:213 -msgid "Video number" +#: rapid/problemnotification.py:259 +#, python-format +msgid "An error occurred when backing up on %(volume)s." msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata -#: rapid/renamesubfolderprefs.py:215 -msgid "Aperture" +#: rapid/problemnotification.py:261 +msgid "Errors occurred when backing up on the following backup devices: " msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata -#: rapid/renamesubfolderprefs.py:217 -msgid "ISO" +#: rapid/problemnotification.py:265 rapid/problemnotification.py:313 +#, python-format +msgid "%(volume)s (%(inst)s), " msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata -#: rapid/renamesubfolderprefs.py:219 -msgid "Exposure time" +#: rapid/problemnotification.py:267 +#, python-format +msgid "%(volume)s, " msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata -#: rapid/renamesubfolderprefs.py:221 -msgid "Focal length" +#: rapid/problemnotification.py:271 rapid/problemnotification.py:315 +#, python-format +msgid "%(volumes)s and %(volume)s (%(inst)s)." msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata -#: rapid/renamesubfolderprefs.py:223 -msgid "Camera make" +#: rapid/problemnotification.py:276 +#, python-format +msgid "%(volumes)s and %(volume)s." msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata -#: rapid/renamesubfolderprefs.py:225 -msgid "Camera model" +#: rapid/problemnotification.py:284 +#, python-format +msgid "Backup already exists on %(volume)s." msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata -#: rapid/renamesubfolderprefs.py:227 -msgid "Short camera model" +#: rapid/problemnotification.py:286 +msgid "Backups already exist in these locations: " msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata -#: rapid/renamesubfolderprefs.py:229 -msgid "Hyphenated short camera model" +#: rapid/problemnotification.py:296 +#, python-format +msgid "Backup overwritten on %(volume)s." msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata -#: rapid/renamesubfolderprefs.py:231 -msgid "Serial number" +#: rapid/problemnotification.py:298 +msgid "Backups overwritten on these devices: " msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata -#: rapid/renamesubfolderprefs.py:233 -msgid "Shutter count" +#: rapid/problemnotification.py:309 +#, python-format +msgid "An error occurred when creating directories on %(volume)s: %(inst)s." msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata -#: rapid/renamesubfolderprefs.py:235 -msgid "Owner name" +#: rapid/problemnotification.py:311 +msgid "" +"Errors occurred when creating directories on the following backup devices: " msgstr "" -#: rapid/renamesubfolderprefs.py:236 -msgid "Codec" +#: rapid/problemnotification.py:322 +#, python-format +msgid "%(previousproblem)s Additionally, %(newproblem)s" msgstr "" -#: rapid/renamesubfolderprefs.py:237 -msgid "Width" +#: rapid/problemnotification.py:330 +#, python-format +msgid " Furthermore, there were %(problems)s." msgstr "" -#: rapid/renamesubfolderprefs.py:238 -msgid "Height" +#: rapid/problemnotification.py:332 +#, python-format +msgid " Furthermore, there was a %(problem)s." msgstr "" -#: rapid/renamesubfolderprefs.py:239 -msgid "Length" +#: rapid/problemnotification.py:341 +#, python-format +msgid "The %(type)s metadata is missing." msgstr "" -#: rapid/renamesubfolderprefs.py:240 -msgid "Frames Per Second" +#: rapid/problemnotification.py:343 +msgid "The following metadata is missing: " msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers -#: rapid/renamesubfolderprefs.py:242 -msgid "Downloads today" +#: rapid/problemnotification.py:346 +#, python-format +msgid "%(missing_metadata_elements)s and %(final_missing_metadata_element)s." msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers -#: rapid/renamesubfolderprefs.py:244 -msgid "Session number" +#: rapid/problemnotification.py:363 +msgid "Problems in subfolder and filename generation" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers -#: rapid/renamesubfolderprefs.py:246 -msgid "Subfolder number" +#: rapid/problemnotification.py:365 +msgid "Problem in subfolder and filename generation" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers -#: rapid/renamesubfolderprefs.py:248 -msgid "Stored number" +#: rapid/problemnotification.py:368 +#, python-format +msgid "Problems in %s generation" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequenceletters -#: rapid/renamesubfolderprefs.py:250 -msgid "Sequence letter" +#: rapid/problemnotification.py:370 +#, python-format +msgid "Problem in %s generation" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename -#: rapid/renamesubfolderprefs.py:252 -msgid "All digits" +#: rapid/problemnotification.py:379 +#, python-format +msgid "%(filetype)s already exists, but it was backed up" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename -#: rapid/renamesubfolderprefs.py:254 -msgid "Last digit" +#: rapid/problemnotification.py:381 +#, python-format +msgid "An error occurred when copying the %(filetype)s, but it was backed up" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename -#: rapid/renamesubfolderprefs.py:256 -msgid "Last 2 digits" +#: rapid/problemnotification.py:401 +msgid "Multiple problems were encountered" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename -#: rapid/renamesubfolderprefs.py:258 -msgid "Last 3 digits" +#: rapid/problemnotification.py:403 +msgid "Photos detected with the same filenames, but taken at different times" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename -#: rapid/renamesubfolderprefs.py:260 -msgid "Last 4 digits" +#: rapid/problemnotification.py:409 +msgid "there were errors backing up" msgstr "" -#. Translators: please not the capitalization of this text, and keep it the same if your language features capitalization -#: rapid/renamesubfolderprefs.py:262 -msgid "Original Case" +#: rapid/problemnotification.py:410 +msgid "There were errors backing up" msgstr "" -#. Translators: please not the capitalization of this text, and keep it the same if your language features capitalization -#: rapid/renamesubfolderprefs.py:264 -msgid "UPPERCASE" +#: rapid/problemnotification.py:412 +msgid "there was an error backing up" msgstr "" -#. Translators: please not the capitalization of this text, and keep it the same if your language features capitalization -#: rapid/renamesubfolderprefs.py:266 -msgid "lowercase" +#: rapid/problemnotification.py:413 +msgid "There was an error backing up" msgstr "" -#: rapid/renamesubfolderprefs.py:267 -msgid "One digit" +#. e.g. +#: rapid/problemnotification.py:416 +#, python-format +msgid "%(previousproblem)s, and %(backinguperror)s" msgstr "" -#: rapid/renamesubfolderprefs.py:268 -msgid "Two digits" +#: rapid/rpdfile.py:110 +msgid "photos or videos" msgstr "" -#: rapid/renamesubfolderprefs.py:269 -msgid "Three digits" +#: rapid/rpdfile.py:134 +#, python-format +msgid "%(number)s %(filetypes)s" msgstr "" -#: rapid/renamesubfolderprefs.py:270 -msgid "Four digits" +#: rapid/rpdfile.py:242 +msgid "Photo" msgstr "" -#: rapid/renamesubfolderprefs.py:271 -msgid "Five digits" +#: rapid/rpdfile.py:268 +msgid "Video" msgstr "" -#: rapid/renamesubfolderprefs.py:272 -msgid "Six digits" +#: rapid/subfolderfile.py:177 +msgid "subfolder and filename" msgstr "" -#: rapid/renamesubfolderprefs.py:273 -msgid "Seven digits" +#: rapid/glade3/rapid.ui.h:1 +msgid "About..." msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:275 -msgid "Subseconds" +#: rapid/glade3/rapid.ui.h:2 +msgid "Check All" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:277 -msgid "YYYYMMDD" +#: rapid/glade3/rapid.ui.h:3 +msgid "Check All Photos" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:279 -msgid "YYYY-MM-DD" +#: rapid/glade3/rapid.ui.h:4 +msgid "Check All Videos" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:281 -msgid "YYMMDD" +#: rapid/glade3/rapid.ui.h:5 +msgid "Download / Pause" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:283 -msgid "YY-MM-DD" +#: rapid/glade3/rapid.ui.h:6 +msgid "Get Help Online..." msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:285 -msgid "MMDDYYYY" +#: rapid/glade3/rapid.ui.h:7 +msgid "Make a Donation..." msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:287 -msgid "MMDDYY" +#: rapid/glade3/rapid.ui.h:8 +msgid "Next File" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:289 -msgid "MMDD" +#: rapid/glade3/rapid.ui.h:9 +msgid "Previous File" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:291 -msgid "DDMMYYYY" +#: rapid/glade3/rapid.ui.h:10 +msgid "Quit" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:293 -msgid "DDMMYY" +#: rapid/glade3/rapid.ui.h:12 +msgid "Refresh" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:295 -msgid "YYYY" +#: rapid/glade3/rapid.ui.h:13 +msgid "Report a Problem..." msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:297 -msgid "YY" +#: rapid/glade3/rapid.ui.h:14 +msgid "Select All Wit_h Job Code" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:299 -msgid "MM" +#: rapid/glade3/rapid.ui.h:15 +msgid "Select All Without _Job Code" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:301 -msgid "DD" +#: rapid/glade3/rapid.ui.h:16 +msgid "Translate this Application..." msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:303 -msgid "HHMMSS" +#: rapid/glade3/rapid.ui.h:17 +msgid "Uncheck All" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:305 -msgid "HHMM" +#: rapid/glade3/rapid.ui.h:18 +msgid "_Check All" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:307 -msgid "HH-MM-SS" +#: rapid/glade3/rapid.ui.h:19 +msgid "_Clear Completed Downloads" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:309 -msgid "HH-MM" +#: rapid/glade3/rapid.ui.h:20 +msgid "_Download" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:311 -msgid "HH" +#: rapid/glade3/rapid.ui.h:21 +msgid "_Error Log" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:313 -msgid "MM (minutes)" +#: rapid/glade3/rapid.ui.h:22 +msgid "_File" msgstr "" -#. Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime -#: rapid/renamesubfolderprefs.py:315 -msgid "SS" +#: rapid/glade3/rapid.ui.h:23 +msgid "_Help" msgstr "" -#: rapid/renamesubfolderprefs.py:800 -#, python-format -msgid "" -"Preference key '%(key)s' is invalid.\n" -"Expected one of %(value)s" +#: rapid/glade3/rapid.ui.h:24 +msgid "_Include in download" msgstr "" -#: rapid/renamesubfolderprefs.py:807 -#, python-format -msgid "Preference value '%(value)s' is invalid" +#: rapid/glade3/rapid.ui.h:25 +msgid "_Make a Donation..." msgstr "" -#: rapid/renamesubfolderprefs.py:811 -msgid "These preferences are not well formed:" +#: rapid/glade3/rapid.ui.h:26 +msgid "_Select" msgstr "" -#: rapid/renamesubfolderprefs.py:1524 -#, python-format -msgid "Subfolder preferences should not start with a %s" +#: rapid/glade3/rapid.ui.h:27 +msgid "_Translate this Application..." msgstr "" -#: rapid/renamesubfolderprefs.py:1526 -#, python-format -msgid "Subfolder preferences should not end with a %s" +#: rapid/glade3/rapid.ui.h:28 +msgid "_Uncheck All" msgstr "" -#: rapid/renamesubfolderprefs.py:1530 -#, python-format -msgid "Subfolder preferences should not contain two %s one after the other" +#: rapid/glade3/rapid.ui.h:29 +msgid "_View" msgstr "" -#: rapid/glade3/rapid.glade.h:1 +#: rapid/glade3/prefs.ui.h:1 msgid " " msgstr "" -#: rapid/glade3/rapid.glade.h:2 -msgid " " -msgstr "" - -#: rapid/glade3/rapid.glade.h:3 +#: rapid/glade3/prefs.ui.h:2 msgid " hh:mm" msgstr "" -#: rapid/glade3/rapid.glade.h:4 +#: rapid/glade3/prefs.ui.h:3 msgid ":" msgstr "" -#: rapid/glade3/rapid.glade.h:5 +#: rapid/glade3/prefs.ui.h:4 msgid "Backup" msgstr "" -#: rapid/glade3/rapid.glade.h:6 +#: rapid/glade3/prefs.ui.h:5 msgid "Compatibility with Other Operating Systems" msgstr "" -#: rapid/glade3/rapid.glade.h:7 +#: rapid/glade3/prefs.ui.h:6 msgid "Devices" msgstr "" -#: rapid/glade3/rapid.glade.h:8 +#: rapid/glade3/prefs.ui.h:7 msgid "Download Folder" msgstr "" -#: rapid/glade3/rapid.glade.h:9 +#: rapid/glade3/prefs.ui.h:8 msgid "Download Subfolders" msgstr "" -#: rapid/glade3/rapid.glade.h:10 +#: rapid/glade3/prefs.ui.h:9 msgid "Example" msgstr "" -#: rapid/glade3/rapid.glade.h:11 +#: rapid/glade3/prefs.ui.h:10 msgid "Job Codes" msgstr "" -#: rapid/glade3/rapid.glade.h:12 +#: rapid/glade3/prefs.ui.h:11 msgid "Photo Rename" msgstr "" -#: rapid/glade3/rapid.glade.h:13 +#: rapid/glade3/prefs.ui.h:12 msgid "Photo and Video Name Conflicts" msgstr "" -#: rapid/glade3/rapid.glade.h:14 +#: rapid/glade3/prefs.ui.h:13 msgid "Program Automation" msgstr "" -#: rapid/glade3/rapid.glade.h:15 +#: rapid/glade3/prefs.ui.h:14 msgid "Sequence Numbers" msgstr "" -#: rapid/glade3/rapid.glade.h:16 +#: rapid/glade3/prefs.ui.h:15 msgid "/media/externaldrive/Photos" msgstr "" -#: rapid/glade3/rapid.glade.h:17 +#: rapid/glade3/prefs.ui.h:16 msgid "Example: /home/user/Pictures" msgstr "" -#: rapid/glade3/rapid.glade.h:18 +#: rapid/glade3/prefs.ui.h:17 msgid "Example:" msgstr "" -#: rapid/glade3/rapid.glade.h:19 +#: rapid/glade3/prefs.ui.h:18 msgid "New:" msgstr "" -#: rapid/glade3/rapid.glade.h:20 +#: rapid/glade3/prefs.ui.h:19 msgid "Original:" msgstr "" -#: rapid/glade3/rapid.glade.h:21 -msgid "Automation" -msgstr "" - -#: rapid/glade3/rapid.glade.h:22 +#: rapid/glade3/prefs.ui.h:20 msgid "Backup\t" msgstr "" -#: rapid/glade3/rapid.glade.h:23 +#: rapid/glade3/prefs.ui.h:21 msgid "Devices" msgstr "" -#: rapid/glade3/rapid.glade.h:24 +#: rapid/glade3/prefs.ui.h:22 msgid "Error Handling" msgstr "" -#: rapid/glade3/rapid.glade.h:25 +#: rapid/glade3/prefs.ui.h:23 msgid "Job Codes" msgstr "" -#: rapid/glade3/rapid.glade.h:26 +#: rapid/glade3/prefs.ui.h:24 +msgid "Miscellaneous" +msgstr "" + +#: rapid/glade3/prefs.ui.h:25 msgid "Photo Download Folders" msgstr "" -#: rapid/glade3/rapid.glade.h:27 +#: rapid/glade3/prefs.ui.h:26 msgid "Photo Rename\t" msgstr "" -#: rapid/glade3/rapid.glade.h:28 +#: rapid/glade3/prefs.ui.h:27 msgid "Rename Options" msgstr "" -#: rapid/glade3/rapid.glade.h:29 +#: rapid/glade3/prefs.ui.h:28 msgid "Video Download Folders" msgstr "" -#: rapid/glade3/rapid.glade.h:30 +#: rapid/glade3/prefs.ui.h:29 msgid "Video Rename\t" msgstr "" -#: rapid/glade3/rapid.glade.h:31 +#: rapid/glade3/prefs.ui.h:30 msgid "Add unique identifier" msgstr "" -#: rapid/glade3/rapid.glade.h:32 +#: rapid/glade3/prefs.ui.h:31 msgid "Automatically detect Portable Storage Devices" msgstr "" -#: rapid/glade3/rapid.glade.h:33 +#: rapid/glade3/prefs.ui.h:32 msgid "Automatically detect backup devices" msgstr "" -#: rapid/glade3/rapid.glade.h:34 +#: rapid/glade3/prefs.ui.h:33 msgid "Automatically detect devices" msgstr "" -#: rapid/glade3/rapid.glade.h:35 -msgid "Automation" -msgstr "" - -#: rapid/glade3/rapid.glade.h:36 +#: rapid/glade3/prefs.ui.h:34 msgid "Backup" msgstr "" -#: rapid/glade3/rapid.glade.h:37 +#: rapid/glade3/prefs.ui.h:35 msgid "Backup location:" msgstr "" -#: rapid/glade3/rapid.glade.h:38 +#: rapid/glade3/prefs.ui.h:36 msgid "Backup photos and videos when downloading" msgstr "" -#: rapid/glade3/rapid.glade.h:39 +#: rapid/glade3/prefs.ui.h:37 msgid "" "Choose the download folder. Subfolders for the downloaded photos will be " "automatically created in this folder using the structure specified below." msgstr "" -#: rapid/glade3/rapid.glade.h:40 +#: rapid/glade3/prefs.ui.h:38 msgid "" "Choose the download folder. Subfolders for the downloaded videos will be " "automatically created in this folder using the structure specified below." msgstr "" -#: rapid/glade3/rapid.glade.h:41 -msgid "Copyright Damon Lynch 2007-10" -msgstr "" - -#: rapid/glade3/rapid.glade.h:42 +#: rapid/glade3/prefs.ui.h:39 msgid "Day start:" msgstr "" -#: rapid/glade3/rapid.glade.h:43 +#: rapid/glade3/prefs.ui.h:40 msgid "Delete photos and videos from device upon download completion" msgstr "" -#: rapid/glade3/rapid.glade.h:44 +#: rapid/glade3/prefs.ui.h:41 msgid "Devices" msgstr "" -#: rapid/glade3/rapid.glade.h:45 +#: rapid/glade3/prefs.ui.h:42 msgid "" "Devices are from where to download photos and videos, such as cameras, " "memory cards or Portable Storage Devices.\n" @@ -1632,152 +1345,99 @@ msgid "" "reader." msgstr "" -#: rapid/glade3/rapid.glade.h:50 -msgid "Download / Pause" -msgstr "" - -#: rapid/glade3/rapid.glade.h:51 +#: rapid/glade3/prefs.ui.h:47 msgid "Download folder:" msgstr "" -#: rapid/glade3/rapid.glade.h:52 +#: rapid/glade3/prefs.ui.h:48 msgid "Downloads today:" msgstr "" -#: rapid/glade3/rapid.glade.h:53 +#: rapid/glade3/prefs.ui.h:49 msgid "Error Handling" msgstr "" -#: rapid/glade3/rapid.glade.h:54 -msgid "Error Log" +#: rapid/glade3/prefs.ui.h:50 +msgid "Exit program even if download had warnings or errors" msgstr "" -#: rapid/glade3/rapid.glade.h:55 -msgid "Exit program if download completes without any warnings or errors" +#: rapid/glade3/prefs.ui.h:51 +msgid "Exit program when download completes" msgstr "" -#: rapid/glade3/rapid.glade.h:56 +#: rapid/glade3/prefs.ui.h:52 msgid "If you disable automatic detection, choose the exact backup location." msgstr "" -#: rapid/glade3/rapid.glade.h:57 +#: rapid/glade3/prefs.ui.h:53 msgid "" "If you disable automatic detection, choose the exact location of the images " "and videos." msgstr "" -#: rapid/glade3/rapid.glade.h:58 +#: rapid/glade3/prefs.ui.h:54 msgid "" "If you enable automatic detection of Portable Storage Devices, the entire " "device will be scanned for images. On large devices, this could take some " "time." msgstr "" -#: rapid/glade3/rapid.glade.h:59 -msgid "Import your photos and videos efficiently and reliably" -msgstr "" - -#: rapid/glade3/rapid.glade.h:60 +#: rapid/glade3/prefs.ui.h:55 msgid "Job Codes" msgstr "" -#: rapid/glade3/rapid.glade.h:61 +#: rapid/glade3/prefs.ui.h:56 msgid "Location:" msgstr "" -#: rapid/glade3/rapid.glade.h:62 -msgid "Overwrite" +#: rapid/glade3/prefs.ui.h:57 +msgid "Miscillaneous" msgstr "" -#: rapid/glade3/rapid.glade.h:63 -msgid "P_review Columns" +#: rapid/glade3/prefs.ui.h:58 +msgid "Overwrite" msgstr "" -#: rapid/glade3/rapid.glade.h:64 +#: rapid/glade3/prefs.ui.h:59 msgid "Photo Folders" msgstr "" -#: rapid/glade3/rapid.glade.h:65 +#: rapid/glade3/prefs.ui.h:60 msgid "Photo Rename" msgstr "" -#: rapid/glade3/rapid.glade.h:66 +#: rapid/glade3/prefs.ui.h:61 msgid "Photo backup folder name:" msgstr "" -#: rapid/glade3/rapid.glade.h:67 +#: rapid/glade3/prefs.ui.h:62 msgid "Preferences: Rapid Photo Downloader" msgstr "" -#: rapid/glade3/rapid.glade.h:68 -msgid "Preview _Folders" -msgstr "" - -#: rapid/glade3/rapid.glade.h:70 +#: rapid/glade3/prefs.ui.h:64 msgid "R_emove All" msgstr "" -#: rapid/glade3/rapid.glade.h:72 -msgid "" -"Rapid Photo Downloader is free software; you can redistribute it and/or " -"modify it under the terms of the GNU General Public License as published by " -"the Free Software Foundation; either version 2 of the License, or (at your " -"option) any later version.\n" -"\n" -"Rapid Photo Downloader is distributed in the hope that it will be useful, " -"but WITHOUT ANY WARRANTY; without even the implied warranty of " -"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General " -"Public License for more details.\n" -"\n" -"You should have received a copy of the GNU General Public License along with " -"Rapid Photo Downloader; if not, write to the Free Software Foundation, Inc., " -"51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA." -msgstr "" - -#: rapid/glade3/rapid.glade.h:77 +#: rapid/glade3/prefs.ui.h:65 msgid "Rename Options" msgstr "" -#: rapid/glade3/rapid.glade.h:78 -msgid "Se_lect None" -msgstr "" - -#: rapid/glade3/rapid.glade.h:79 -msgid "Select All Pho_tos" -msgstr "" - -#: rapid/glade3/rapid.glade.h:80 -msgid "Select All Vi_deos" -msgstr "" - -#: rapid/glade3/rapid.glade.h:81 -msgid "Select All Wit_h Job Code" -msgstr "" - -#: rapid/glade3/rapid.glade.h:82 -msgid "Select All Without _Job Code" -msgstr "" - -#: rapid/glade3/rapid.glade.h:83 -msgid "Select _All" -msgstr "" - -#: rapid/glade3/rapid.glade.h:84 +#: rapid/glade3/prefs.ui.h:66 msgid "Skip" msgstr "" -#: rapid/glade3/rapid.glade.h:85 +#: rapid/glade3/prefs.ui.h:67 msgid "Skip download" msgstr "" -#: rapid/glade3/rapid.glade.h:86 +#: rapid/glade3/prefs.ui.h:68 msgid "" "Sorry, video downloading functionality disabled. To download videos, please " "install the hachoir metadata and kaa metadata packages for " "python." msgstr "" -#: rapid/glade3/rapid.glade.h:87 +#: rapid/glade3/prefs.ui.h:69 msgid "" "Specify the folder in which backups are stored on the device. \n" "\n" @@ -1786,136 +1446,76 @@ msgid "" "a folder in it with one of these names." msgstr "" -#: rapid/glade3/rapid.glade.h:90 +#: rapid/glade3/prefs.ui.h:72 msgid "" "Specify the time in 24 hour format at which the Downloads today " "sequence number should be reset." msgstr "" -#: rapid/glade3/rapid.glade.h:91 +#: rapid/glade3/prefs.ui.h:73 msgid "" "Specify whether photo, video and folder names should have any characters " "removed that are not allowed by other operating systems." msgstr "" -#: rapid/glade3/rapid.glade.h:92 +#: rapid/glade3/prefs.ui.h:74 msgid "Start downloading at program startup" msgstr "" -#: rapid/glade3/rapid.glade.h:93 +#: rapid/glade3/prefs.ui.h:75 msgid "Start downloading upon device insertion" msgstr "" -#: rapid/glade3/rapid.glade.h:94 +#: rapid/glade3/prefs.ui.h:76 msgid "Stored number:" msgstr "" -#: rapid/glade3/rapid.glade.h:95 +#: rapid/glade3/prefs.ui.h:77 msgid "Strip incompatible characters" msgstr "" -#: rapid/glade3/rapid.glade.h:96 +#: rapid/glade3/prefs.ui.h:78 msgid "Synchronize RAW + JPEG sequence numbers" msgstr "" -#: rapid/glade3/rapid.glade.h:97 +#: rapid/glade3/prefs.ui.h:79 msgid "Unmount (\"eject\") device upon download completion" msgstr "" -#: rapid/glade3/rapid.glade.h:98 +#: rapid/glade3/prefs.ui.h:80 msgid "Video Folders" msgstr "" -#: rapid/glade3/rapid.glade.h:99 +#: rapid/glade3/prefs.ui.h:81 msgid "Video Rename" msgstr "" -#: rapid/glade3/rapid.glade.h:100 +#: rapid/glade3/prefs.ui.h:82 msgid "Video backup folder name:" msgstr "" -#: rapid/glade3/rapid.glade.h:101 +#: rapid/glade3/prefs.ui.h:83 msgid "" "When a photo or video of the same name has already been downloaded, choose " "whether to skip downloading the file, or to add a unique indentifier." msgstr "" -#: rapid/glade3/rapid.glade.h:102 +#: rapid/glade3/prefs.ui.h:84 msgid "" "When backing up, choose whether to overwrite a file on the backup device " "that has the same name, or skip backing it up." msgstr "" -#: rapid/glade3/rapid.glade.h:103 +#: rapid/glade3/prefs.ui.h:85 msgid "" "You can have your photos and videos backed up to multiple locations as they " "are downloaded, e.g. external hard drives." msgstr "" -#: rapid/glade3/rapid.glade.h:104 +#: rapid/glade3/prefs.ui.h:86 msgid "_Add..." msgstr "" -#: rapid/glade3/rapid.glade.h:105 -msgid "_Clear Completed Downloads" -msgstr "" - -#: rapid/glade3/rapid.glade.h:106 -msgid "_Device" -msgstr "" - -#: rapid/glade3/rapid.glade.h:107 -msgid "_Error Log" -msgstr "" - -#: rapid/glade3/rapid.glade.h:108 -msgid "_File" -msgstr "" - -#: rapid/glade3/rapid.glade.h:109 -msgid "_Filename" -msgstr "" - -#: rapid/glade3/rapid.glade.h:110 -msgid "_Get Help Online..." -msgstr "" - -#: rapid/glade3/rapid.glade.h:111 -msgid "_Help" -msgstr "" - -#: rapid/glade3/rapid.glade.h:112 -msgid "_Make a Donation..." -msgstr "" - -#: rapid/glade3/rapid.glade.h:113 -msgid "_Path" -msgstr "" - -#: rapid/glade3/rapid.glade.h:114 -msgid "_Preview" -msgstr "" - -#: rapid/glade3/rapid.glade.h:115 -msgid "_Report a Problem..." -msgstr "" - -#: rapid/glade3/rapid.glade.h:116 -msgid "_Select" -msgstr "" - -#: rapid/glade3/rapid.glade.h:117 -msgid "_Size" -msgstr "" - -#: rapid/glade3/rapid.glade.h:118 -msgid "_Translate this Application..." -msgstr "" - -#: rapid/glade3/rapid.glade.h:119 -msgid "_Type" -msgstr "" - -#: rapid/glade3/rapid.glade.h:120 -msgid "_View" +#: rapid/glade3/about.ui.h:1 +msgid "Import your photos and videos efficiently and reliably" msgstr "" diff --git a/rapid-photo-downloader b/rapid-photo-downloader index 87e63c0..b37ddf8 100755 --- a/rapid-photo-downloader +++ b/rapid-photo-downloader @@ -2,4 +2,4 @@ import rapid.rapid -rapid.rapid.start () +rapid.rapid.start() diff --git a/rapid/ChangeLog b/rapid/ChangeLog index a1b7070..0234613 100644 --- a/rapid/ChangeLog +++ b/rapid/ChangeLog @@ -1,9 +1,49 @@ +Version 0.4.0 alpha 1 +--------------------- + +2011-03-24 + +Rapid Photo Downloader is much faster and sports a new user interface. It is +about 50 times faster in tasks like scanning photos and videos before the +download. It also performs the actual downloads quicker. It will use +multiple CPU cores if they are available. + +Rapid Photo Downloader now requires version 0.3.0 or newer of pyexiv2. It also +requries Python Imaging (PIL) to run. It will only run on recent Linux +distributions such as Ubuntu 10.04 or newer. It has been tested on Ubuntu 10.04, +10.10 and 11.04, as well as Fedora 14. (There is currently an unusual bug +adjusting some preferences when running Ubuntu 11.04. See bug #739021). + +This is an alpha release because it is missing features that are present in +version 0.3.5. Missing features include: + +* System Notifications of download completion +* Job Codes +* Backups as you download +* Automation features, e.g. automatically start download at startup +* Error log window (currently you must check the command line for error output) +* Time remaining status messages +* Synchronize RAW + JPEG Sequence Numbers +* Add unique identifier to a filename if a file with the same name already + exists +* Sample file names and subfolders are not displayed in the preferences window + +These missing features will be added in subsequent alpha and beta releases. + +Kaa-metadata is no longer required to download videos. However, if you +want to use Frames Per Second or Codec metadata information in subfolder or +video file names, you must ensure it is installed. This is no longer checked at +program startup. + +Thanks go to Robert Park for refreshing the tranlsations code. + + Version 0.3.5 ------------- 2011-03-23 -The primary purpose of this release is update translations and fix bug #714039, +The primrary purpose of this release is update translations and fix bug #714039, where under certain circumstances the program could crash while downloading files. diff --git a/rapid/common.py b/rapid/common.py deleted file mode 100644 index a55d835..0000000 --- a/rapid/common.py +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/python -# -*- coding: latin1 -*- - -### Copyright (C) 2007-09 Damon Lynch - -### This program is free software; you can redistribute it and/or modify -### it under the terms of the GNU General Public License as published by -### the Free Software Foundation; either version 2 of the License, or -### (at your option) any later version. - -### This program is distributed in the hope that it will be useful, -### but WITHOUT ANY WARRANTY; without even the implied warranty of -### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -### GNU General Public License for more details. - -### You should have received a copy of the GNU General Public License -### along with this program; if not, write to the Free Software -### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -import os -import sys -import gc -import distutils.version -import gtk.gdk as gdk -import gtk -try: - import gio -except: - pass - -import config - -import locale -import gettext - -class Configi18n: - """ Setup translation - - Adapated from code example of Mark Mruss http://www.learningpython.com. - Unlike his example, this code uses a local locale directory only if the environment - variable LOCALEDIR has been set to some or other value. - """ - - # Do not put this code block in __init__, because it needs to be run only once - - # if the evironment value 'LOCAELDIR' is set, then use this as the source of translation data - # otherwise, rely on the system-wide data - locale_path = os.environ.get('LOCALEDIR', None) - - # Init the list of languages to support - langs = [] - #Check the default locale - lc, encoding = locale.getdefaultlocale() - if (lc): - #If we have a default, it's the first in the list - langs = [lc] - # Now let's get all of the supported languages on the system - language = os.environ.get('LANGUAGE', None) - if (language): - # langage comes back something like en_CA:en_US:en_GB:en - langs += language.split(":") - - # add on to the back of the list the translations that we know that we have, our defaults - langs += ["en_US"] - - # Now langs is a list of all of the languages that we are going - # to try to use. First we check the default, then what the system - # told us, and finally the 'known' list - - gettext.bindtextdomain(config.APP_NAME, locale_path) - gettext.textdomain(config.APP_NAME) - # Get the language to use - lang = gettext.translation(config.APP_NAME, locale_path, languages=langs, fallback = True) - # Install the language, map _() (which we marked our - # strings to translate with) to self.lang.gettext() which will - # translate them. - _ = lang.gettext - - -def pythonifyVersion(v): - """ makes version number a version number in distutils sense""" - return distutils.version.StrictVersion(v.replace( '~','')) - -def getFullProgramName(): - """ return the full name of the process running """ - return os.path.basename(sys.argv[0]) - -def getProgramName(): - """ return the name of the process running, removing the .py extension if it exists """ - programName = getFullProgramName() - if programName.find('.py') > 0: - programName = programName[:programName.find('.py')] - return programName - -def splitDirectories(directories): - """ split directories specified in string into a list """ - if directories.find(',') > 0: - d = directories.split(',') - else: - d = directories.split() - directories = [] - for i in d: - directories.append(i.strip()) - return directories - - - -def getFullPath(path): - """ make path relative to home directory if not an absolute path """ - if os.path.isabs(path): - return path - else: - return os.path.join(os.path.expanduser('~'), path) - - -def escape(s): - """ - Replace special characters by SGML entities. - """ - entities = ("&&", "<<", ">>") - for e in entities: - s = s.replace(e[0], e[1:]) - return s - -def formatSizeForUser(bytes, zeroString="", withDecimals=True, kbOnly=False): - """Format an int containing the number of bytes into a string suitable for - printing out to the user. zeroString is the string to use if bytes == 0. - source: https://develop.participatoryculture.org/trac/democracy/browser/trunk/tv/portable/util.py?rev=3993 - - """ - if bytes > (1 << 30) and not kbOnly: - value = (bytes / (1024.0 * 1024.0 * 1024.0)) - if withDecimals: - format = "%1.1fGB" - else: - format = "%dGB" - elif bytes > (1 << 20) and not kbOnly: - value = (bytes / (1024.0 * 1024.0)) - if withDecimals: - format = "%1.1fMB" - else: - format = "%dMB" - elif bytes > (1 << 10): - value = (bytes / 1024.0) - if withDecimals: - format = "%1.1fKB" - else: - format = "%dKB" - elif bytes > 1: - value = bytes - if withDecimals: - format = "%1.1fB" - else: - format = "%dB" - else: - return zeroString - return format % value - -def scale2pixbuf(width_max, height_max, pixbuf, return_size=False): - """ - Scale to width_max and height_max. - Keep aspect ratio. - Code adapted from gthumpy, by guettli - """ - - width_orig = float(pixbuf.get_width()) - height_orig = float(pixbuf.get_height()) - if (width_orig / width_max) > (height_orig / height_max): - height = int((height_orig / width_orig) * width_max) - width = width_max - else: - width = int((width_orig / height_orig) * height_max) - height=height_max - - pixbuf = pixbuf.scale_simple(width, height, gdk.INTERP_BILINEAR) - gc.collect() # Tell Python to clean up the memory - if return_size: - return pixbuf, width_orig, height_orig - return pixbuf - -def get_icon_pixbuf(using_gio, icon, size, fallback='gtk-harddisk'): - """ returns icon for the volume, or None if not available""" - - icontheme = gtk.icon_theme_get_default() - - if using_gio: - f = None - if isinstance(icon, gio.ThemedIcon): - try: - # on some user's systems, themes do not have icons associated with them - iconinfo = icontheme.choose_icon(icon.get_names(), size, gtk.ICON_LOOKUP_USE_BUILTIN) - f = iconinfo.get_filename() - v = gtk.gdk.pixbuf_new_from_file_at_size(f, size, size) - except: - f = None - if not f: - v = icontheme.load_icon(fallback, size, gtk.ICON_LOOKUP_USE_BUILTIN) - else: - v = icontheme.load_icon(icon, size, gtk.ICON_LOOKUP_USE_BUILTIN) - return v - -def register_iconsets(icon_info): - """ - Register icons in the icon set if they're not already used - - From http://faq.pygtk.org/index.py?req=show&file=faq08.012.htp - """ - - iconfactory = gtk.IconFactory() - stock_ids = gtk.stock_list_ids() - for stock_id, file in icon_info: - # only load image files when our stock_id is not present - if stock_id not in stock_ids: - pixbuf = gtk.gdk.pixbuf_new_from_file(file) - iconset = gtk.IconSet(pixbuf) - iconfactory.add(stock_id, iconset) - iconfactory.add_default() - - - - -if __name__ == '__main__': - i = Configi18n() - _ = i._ - print _("hello world") diff --git a/rapid/config.py b/rapid/config.py index 626ae33..9f0b6b1 100644 --- a/rapid/config.py +++ b/rapid/config.py @@ -1,5 +1,5 @@ # -*- coding: latin1 -*- -### Copyright (C) 2007, 2008, 2009, 2010 Damon Lynch +### Copyright (C) 2007, 2008, 2009, 2010, 2011 Damon Lynch ### This program is free software; you can redistribute it and/or modify ### it under the terms of the GNU General Public License as published by @@ -15,10 +15,9 @@ ### along with this program; if not, write to the Free Software ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -version = '0.3.5' +version = '0.4.0~a1' GCONF_KEY="/apps/rapid-photo-downloader" -GLADE_FILE = "glade3/rapid.glade" DBUS_NAME = "net.damonlynch.RapidPhotoDownloader" @@ -38,18 +37,10 @@ DEFAULT_VIDEO_BACKUP_LOCATION = 'Videos' DEFAULT_VIDEO_LOCATIONS = ['Videos'] -MAX_NO_READERS = 20 - CRITICAL_ERROR = 1 SERIOUS_ERROR = 2 WARNING = 3 -MAX_LENGTH_DEVICE_NAME = 15 - -MIN_THUMBNAIL_SIZE = 80 -max_thumbnail_size = 320 # will be overridden when the screen is tiny -THUMBNAIL_INCREMENT = 50 - STATUS_DOWNLOAD_PENDING = 0 # going to try to download it STATUS_DOWNLOADED = 1 # downloaded successfully STATUS_DOWNLOADED_WITH_WARNING = 2 # downloaded ok but there was a warning @@ -60,4 +51,7 @@ STATUS_DOWNLOAD_FAILED = 6 # tried to download but failed STATUS_WARNING = 7 # warning (shown in pre-download preview) STATUS_CANNOT_DOWNLOAD = 8 # cannot be downloaded -TINY_SCREEN_HEIGHT = 650 +DEFAULT_WINDOW_WIDTH = 730 +DEFAULT_WINDOW_HEIGHT = 650 + + diff --git a/rapid/copyfiles.py b/rapid/copyfiles.py new file mode 100644 index 0000000..f9ebc60 --- /dev/null +++ b/rapid/copyfiles.py @@ -0,0 +1,170 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import multiprocessing +import tempfile +import os + +import gio + +import logging +logger = multiprocessing.get_logger() + +import rpdmultiprocessing as rpdmp +import rpdfile +import problemnotification as pn +import config + + +from gettext import gettext as _ + + +class CopyFiles(multiprocessing.Process): + def __init__(self, photo_download_folder, video_download_folder, files, + scan_pid, + batch_size_MB, results_pipe, terminate_queue, + run_event): + multiprocessing.Process.__init__(self) + self.results_pipe = results_pipe + self.terminate_queue = terminate_queue + self.batch_size_bytes = batch_size_MB * 1048576 # * 1024 * 1024 + self.photo_download_folder = photo_download_folder + self.video_download_folder = video_download_folder + self.files = files + self.scan_pid = scan_pid + self.no_files= len(self.files) + self.run_event = run_event + + def check_termination_request(self): + """ + Check to see this process has not been requested to immediately terminate + """ + if not self.terminate_queue.empty(): + x = self.terminate_queue.get() + # terminate immediately + logger.info("Terminating file copying") + return True + return False + + + def update_progress(self, amount_downloaded, total): + if (amount_downloaded - self.bytes_downloaded > self.batch_size_bytes) or (amount_downloaded == total): + chunk_downloaded = amount_downloaded - self.bytes_downloaded + self.bytes_downloaded = amount_downloaded + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_BYTES, (self.scan_pid, self.total_downloaded + amount_downloaded)))) + + def progress_callback(self, amount_downloaded, total): + + if self.check_termination_request(): + # FIXME: cancel copy + pass + + self.update_progress(amount_downloaded, total) + + + def run(self): + """start the actual copying of files""" + + self.bytes_downloaded = 0 + self.total_downloaded = 0 + + self.create_temp_dirs() + + # Send the location of both temporary directories, so they can be + # removed once another process attempts to rename all the files in them + # and move them to generated subfolders + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_TEMP_DIRS, + (self.scan_pid, + self.photo_temp_dir, + self.video_temp_dir)))) + + if self.photo_temp_dir or self.video_temp_dir: + for i in range(len(self.files)): + rpd_file = self.files[i] + + # pause if instructed by the caller + self.run_event.wait() + + if self.check_termination_request(): + return None + + source = gio.File(path=rpd_file.full_file_name) + temp_full_file_name = os.path.join( + self._get_dest_dir(rpd_file.file_type), + rpd_file.name) + rpd_file.temp_full_file_name = temp_full_file_name + dest = gio.File(path=temp_full_file_name) + + copy_succeeded = False + try: + source.copy(dest, self.progress_callback, cancellable=None) + copy_succeeded = True + except gio.Error, inst: + rpd_file.add_problem(None, + pn.DOWNLOAD_COPYING_ERROR_W_NO, + {'filetype': rpd_file.title}) + rpd_file.add_extra_detail( + pn.DOWNLOAD_COPYING_ERROR_W_NO_DETAIL, + {'errorno': inst.code, 'strerror': inst.message}) + + rpd_file.status = config.STATUS_DOWNLOAD_FAILED + logger.error("Failed to download file: %s", rpd_file.full_file_name) + logger.error(inst) + self.update_progress(rpd_file.size, rpd_file.size) + + # increment this amount regardless of whether the copy actually + # succeeded or not. It's neccessary to keep the user informed. + self.total_downloaded += rpd_file.size + + + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_FILE, + (copy_succeeded, rpd_file, i + 1, temp_full_file_name)))) + + + self.results_pipe.send((rpdmp.CONN_COMPLETE, None)) + + + def _get_dest_dir(self, file_type): + if file_type == rpdfile.FILE_TYPE_PHOTO: + return self.photo_temp_dir + else: + return self.video_temp_dir + + def _create_temp_dir(self, folder): + try: + temp_dir = tempfile.mkdtemp(prefix="rpd-tmp-", dir=folder) + except OSError, (errno, strerror): + # FIXME: error reporting + logger.error("Failed to create temporary directory in %s: %s %s", + errono, + strerror, + folder) + temp_dir = None + + return temp_dir + + def create_temp_dirs(self): + self.photo_temp_dir = self.video_temp_dir = None + if self.photo_download_folder is not None: + self.photo_temp_dir = self._create_temp_dir(self.photo_download_folder) + if self.video_download_folder is not None: + self.video_temp_dir = self._create_temp_dir(self.photo_download_folder) + + + diff --git a/rapid/device.py b/rapid/device.py new file mode 100644 index 0000000..dcfdf94 --- /dev/null +++ b/rapid/device.py @@ -0,0 +1,176 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import gtk, gio + +import multiprocessing +import logging +logger = multiprocessing.get_logger() + +import paths +import utilities + +from gettext import gettext as _ + +class Device: + def __init__(self, mount=None, path=None): + self.mount = mount + self.path = path + + def get_path(self): + if self.mount: + return self.mount.get_root().get_path() + else: + return self.path + + def get_name(self): + if self.mount: + return self.mount.get_name() + else: + return self.path + + def get_icon(self, size=16): + if self.mount: + icon = self.mount.get_icon() + else: + folder = gio.File(self.path) + file_info = folder.query_info(gio.FILE_ATTRIBUTE_STANDARD_ICON) + icon = file_info.get_icon() + + icontheme = gtk.icon_theme_get_default() + + icon_file = None + if isinstance(icon, gio.ThemedIcon): + try: + # on some user's systems, themes do not have icons associated with them + iconinfo = icontheme.choose_icon(icon.get_names(), size, gtk.ICON_LOOKUP_USE_BUILTIN) + icon_file = iconinfo.get_filename() + return gtk.gdk.pixbuf_new_from_file_at_size(icon_file, size, size) + except: + pass + + if not icon_file: + return icontheme.load_icon('folder', size, gtk.ICON_LOOKUP_USE_BUILTIN) + + +class UseDeviceDialog(gtk.Dialog): + """ + Simple dialog window that prompt's the user whether to use a certain + device or not + """ + def __init__(self, parent_window, device, post_choice_callback): + gtk.Dialog.__init__(self, _('Device Detected'), None, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_NO, gtk.RESPONSE_CANCEL, + gtk.STOCK_YES, gtk.RESPONSE_OK)) + + self.post_choice_callback = post_choice_callback + + self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt + prompt_label = gtk.Label(_('Should this device or partition be used to download photos or videos from?')) + prompt_label.set_line_wrap(True) + prompt_hbox = gtk.HBox() + prompt_hbox.pack_start(prompt_label, False, False, padding=6) + device_label = gtk.Label() + device_label.set_markup("%s" % device.get_name()) + device_hbox = gtk.HBox() + device_hbox.pack_start(device_label, False, False) + path_label = gtk.Label() + path_label.set_markup("%s" % device.get_path()) + path_hbox = gtk.HBox() + path_hbox.pack_start(path_label, False, False) + + icon = device.get_icon(size=36) + if icon: + image = gtk.Image() + image.set_from_pixbuf(icon) + + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt + self.always_checkbutton = gtk.CheckButton(_('_Remember this choice'), True) + + if icon: + device_hbox_icon = gtk.HBox(homogeneous=False, spacing=6) + device_hbox_icon.pack_start(image, False, False, padding = 6) + device_vbox = gtk.VBox(homogeneous=True, spacing=6) + device_vbox.pack_start(device_hbox, False, False) + device_vbox.pack_start(path_hbox, False, False) + device_hbox_icon.pack_start(device_vbox, False, False) + self.vbox.pack_start(device_hbox_icon, padding = 6) + else: + self.vbox.pack_start(device_hbox, padding=6) + self.vbox.pack_start(path_hbox, padding = 6) + + self.vbox.pack_start(prompt_hbox, padding=6) + self.vbox.pack_start(self.always_checkbutton, padding=6) + + self.set_border_width(6) + self.set_has_separator(False) + + self.set_default_response(gtk.RESPONSE_OK) + + + self.set_transient_for(parent_window) + self.show_all() + self.device = device + + self.connect('response', self.on_response) + + def on_response(self, device_dialog, response): + user_selected = False + permanent_choice = self.always_checkbutton.get_active() + if response == gtk.RESPONSE_OK: + user_selected = True + logger.info("%s selected for downloading from", self.device.get_name()) + if permanent_choice: + logger.info("This device or partition will always be used to download from") + else: + logger.info("%s rejected as a download device", self.device.get_name()) + if permanent_choice: + logger.info("This device or partition will never be used to download from") + + self.post_choice_callback(self, user_selected, permanent_choice, + self.device) + + +def is_DCIM_device(path): + """ Returns true if directory specifies media with photos on it""" + + test_path = os.path.join(path, "DCIM") + return utilities.is_directory(test_path) + +def is_backup_media(path, identifiers, writeable=True): + """ Test to see if path is used as a backup medium for storing photos or videos + + Identifiers is expected to be a list of folder names to check to see + if the path is a backup path. Only one of them needs to be present + for the path to be considered a backup medium. + + If writeable is True, the directory must be writeable by the user """ + suitable = False + + for identifier in identifiers: + if os.path.isdir(os.path.join(path, identifier)): + if writeable: + suitable = os.access(os.path.join(path, identifier), os.W_OK) + else: + suitable = True + return suitable + diff --git a/rapid/dropshadow.py b/rapid/dropshadow.py deleted file mode 100755 index 68b5398..0000000 --- a/rapid/dropshadow.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/python - - -import StringIO -import gtk -from PIL import Image, ImageFilter - -def image_to_pixbuf(image): - # this one handles transparency, unlike the default example in the pygtk FAQ - # this is also from the pygtk FAQ - IS_RGBA = image.mode=='RGBA' - return gtk.gdk.pixbuf_new_from_data( - image.tostring(), # data - gtk.gdk.COLORSPACE_RGB, # color mode - IS_RGBA, # has alpha - 8, # bits - image.size[0], # width - image.size[1], # height - (IS_RGBA and 4 or 3) * image.size[0] # rowstride - ) - - -def image_to_pixbuf_no_transparency(image): - fd = StringIO.StringIO() - image.save(fd, "ppm") - contents = fd.getvalue() - fd.close() - loader = gtk.gdk.PixbufLoader("pnm") - loader.write(contents, len(contents)) - pixbuf = loader.get_pixbuf() - loader.close() - return pixbuf - -def pixbuf_to_image(pb): - assert(pb.get_colorspace() == gtk.gdk.COLORSPACE_RGB) - dimensions = pb.get_width(), pb.get_height() - stride = pb.get_rowstride() - pixels = pb.get_pixels() - - mode = pb.get_has_alpha() and "RGBA" or "RGB" - image = Image.frombuffer(mode, dimensions, pixels, - "raw", mode, stride, 1) - - if mode == "RGB": - # convert to having an alpha value, so that the image can - # act as a mask in the drop shadow paste - image = image.convert("RGBA") - - return image - - -class DropShadow(): - """ - Adds a gaussian blur drop shadow to a PIL image. - - Caches backgrounds of particular sizes for improved performance. - - Backgrounds can be made transparent. - - Modification of code from Kevin Schluff and Matimus - License: Python license - See: - http://code.activestate.com/recipes/474116/ (r2) - http://bytes.com/topic/python/answers/606952-pil-paste-image-top-other-dropshadow - - """ - - def __init__(self, offset=(5,5), background_color=0xffffff, shadow = (0x44, 0x44, 0x44, 0xff), - border=8, iterations=3, trim_border=False): - """ - offset - Offset of the shadow from the image as an (x,y) tuple. Can be - positive or negative. - background_color - Background colour behind the image. - shadow - Shadow colour (darkness). - border - Width of the border around the image. This must be wide - enough to account for the blurring of the shadow. - trim_border - If true, the border will only be created on the - sides it needs to be (i.e. only on two sides) - iterations - Number of times to apply the filter. More iterations - produce a more blurred shadow, but increase processing time. - - To make backgrounds transparent, ensure the alpha value of the shadow color is the - same as the background color, e.g. if background_color is 0xffffff, shadow's alpha should be 0xff - - The image must be in RGBA format. - """ - self.backgrounds = {} - self.offset = offset - self.background_color = background_color - self.shadow = shadow - self.border = border - self.trim_border = trim_border - self.iterations = iterations - - if self.offset[0] < 0 or not self.trim_border: - self.left_spacing = self.border - else: - self.left_spacing = 0 - - if self.offset[1] < 0 or not self.trim_border: - self.top_spacing = self.border - else: - self.top_spacing = 0 - - def dropShadow(self, image): - """ - image - The image to overlay on top of the shadow. - """ - dimensions = (image.size[0], image.size[1]) - if not dimensions in self.backgrounds: - - # Create the backdrop image -- a box in the background colour with a - # shadow on it. - - if self.trim_border: - totalWidth = image.size[0] + abs(self.offset[0]) + self.border - totalHeight = image.size[1] + abs(self.offset[1]) + self.border - else: - totalWidth = image.size[0] + abs(self.offset[0]) + 2 * self.border - totalHeight = image.size[1] + abs(self.offset[1]) + 2 * self.border - - back = Image.new("RGBA", (totalWidth, totalHeight), self.background_color) - - # Place the shadow, taking into account the offset from the image - if self.offset[0] > 0 and self.trim_border: - shadowLeft = max(self.offset[0], 0) - else: - shadowLeft = self.border + max(self.offset[0], 0) - if self.offset[1] > 0 and self.trim_border: - shadowTop = max(self.offset[1], 0) - else: - shadowTop = self.border + max(self.offset[1], 0) - - back.paste(self.shadow, [shadowLeft, shadowTop, shadowLeft + image.size[0], - shadowTop + image.size[1]] ) - - # Apply the filter to blur the edges of the shadow. Since a small kernel - # is used, the filter must be applied repeatedly to get a decent blur. - n = 0 - while n < self.iterations: - back = back.filter(ImageFilter.BLUR) - n += 1 - - self.backgrounds[dimensions] = back - - # Paste the input image onto the shadow backdrop - imageLeft = self.left_spacing - min(self.offset[0], 0) - imageTop = self.top_spacing - min(self.offset[1], 0) - - back = self.backgrounds[dimensions].copy() - back.paste(image, (imageLeft, imageTop), image) - - return back - - - -if __name__ == "__main__": - import sys - import os - import common - - - # create another file with a drop shadow - f = sys.argv[1] - - image = Image.open(f) - image.thumbnail((60,36), Image.ANTIALIAS) - image2 = image.copy() - - path, name = os.path.split(f) - name, ext = os.path.splitext(name) - - #image = dropShadow(image, shadow = (0x44, 0x44, 0x44, 0xff)) - dropShadow = DropShadow(offset=(3,3), shadow = (0x34, 0x34, 0x34, 0xff), border=6) - image = dropShadow.dropShadow(image) - image2 = dropShadow.dropShadow(image2) - - nf = os.path.join(path, "%s_small_shadow%s" % (name, ext)) - nf2 = os.path.join(path, "%s_small_shadow2%s" % (name, ext)) - image.save(nf) - image2.save(nf2) - print "wrote %s , %s" % (nf, nf2) - diff --git a/rapid/filmstrip.py b/rapid/filmstrip.py index 275fd1d..fc751fd 100755 --- a/rapid/filmstrip.py +++ b/rapid/filmstrip.py @@ -18,7 +18,7 @@ ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Adds a filmstrip to the left and right of a file +Adds a filmstrip to the left and right of a pixbuf """ import gtk diff --git a/rapid/generatename.py b/rapid/generatename.py new file mode 100644 index 0000000..be4ea6e --- /dev/null +++ b/rapid/generatename.py @@ -0,0 +1,445 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007, 2008, 2009, 2010, 2011 Damon Lynch + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os, re, datetime, string + +import multiprocessing +import logging +logger = multiprocessing.get_logger() + +import problemnotification as pn + +from generatenameconfig import * + +from gettext import gettext as _ + + +def convert_date_for_strftime(datetime_user_choice): + try: + return DATE_TIME_CONVERT[LIST_DATE_TIME_L2.index(datetime_user_choice)] + except: + raise PrefValueInvalidError(datetime_user_choice) + + +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]) + + def _get_date_component(self): + """ + 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': + 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: + delta = datetime.timedelta(days = 1) + d = datetime.datetime.now() - delta + elif self.L1 == DOWNLOAD_TIME: + 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: + return d.strftime(convert_date_for_strftime(self.L2)) + except: + logger.warning("Exif date time value appears invalid for file %s", self.rpd_file.full_file_name) + + # step 3: handle a missing value using file modification time + if self.rpd_file.modification_time: + try: + d = datetime.datetime.fromtimestamp(self.rpd_file.modification_time) + except: + self.rpd_file.add_problem(self.component, pn.INVALID_DATE_TIME, '') + logger.error("Both file modification time and metadata date & time are invalid for file %s", self.rpd_file.full_file_name) + return '' + 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_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 + elif self.L1 == NAME: + filename = name + elif self.L1 == EXTENSION: + if extension: + if not self.strip_initial_period_from_extension: + # keep the period / dot of the extension, so the user does not + # need to manually specify it + filename = extension + else: + # having the period when this is used as a part of a subfolder name + # is a bad idea when it is at the start! + filename = extension[1:] + else: + self.rpd_file.add_problem(self.component, pn.MISSING_FILE_EXTENSION) + return "" + elif self.L1 == IMAGE_NUMBER or self.L1 == VIDEO_NUMBER: + n = re.search("(?P[0-9]+$)", name) + if not n: + self.rpd_file.add_problem(self.component, pn.MISSING_IMAGE_NUMBER) + return '' + else: + image_number = n.group("image_number") + + if self.L2 == IMAGE_NUMBER_ALL: + filename = image_number + elif self.L2 == IMAGE_NUMBER_1: + filename = image_number[-1] + elif self.L2 == IMAGE_NUMBER_2: + filename = image_number[-2:] + elif self.L2 == IMAGE_NUMBER_3: + filename = image_number[-3:] + elif self.L2 == IMAGE_NUMBER_4: + filename = image_number[-4:] + else: + raise TypeError("Incorrect filename option") + + if self.L2 == UPPERCASE: + filename = filename.upper() + elif self.L2 == LOWERCASE: + 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: + v = self.rpd_file.metadata.iso() + elif self.L1 == EXPOSURE_TIME: + v = self.rpd_file.metadata.exposure_time(alternativeFormat=True) + elif self.L1 == FOCAL_LENGTH: + v = self.rpd_file.metadata.focal_length() + elif self.L1 == CAMERA_MAKE: + v = self.rpd_file.metadata.camera_make() + elif self.L1 == CAMERA_MODEL: + v = self.rpd_file.metadata.camera_model() + elif self.L1 == SHORT_CAMERA_MODEL: + v = self.rpd_file.metadata.short_camera_model() + elif self.L1 == SHORT_CAMERA_MODEL_HYPHEN: + v = self.rpd_file.metadata.short_camera_model(includeCharacters = "\-") + elif self.L1 == SERIAL_NUMBER: + v = self.rpd_file.metadata.camera_serial() + elif self.L1 == SHUTTER_COUNT: + v = self.rpd_file.metadata.shutter_count() + if v: + v = int(v) + padding = LIST_SHUTTER_COUNT_L2.index(self.L2) + 3 + formatter = '%0' + str(padding) + "i" + v = formatter % v + + elif self.L1 == OWNER_NAME: + v = self.rpd_file.metadata.owner_name() + else: + raise TypeError("Invalid metadata option specified") + if self.L1 in [CAMERA_MAKE, CAMERA_MODEL, SHORT_CAMERA_MODEL, + SHORT_CAMERA_MODEL_HYPHEN, OWNER_NAME]: + if self.L2 == UPPERCASE: + v = v.upper() + elif self.L2 == LOWERCASE: + v = v.lower() + if not v: + self.rpd_file.add_problem(self.component, pn.MISSING_METADATA, _(self.L1)) + return v + + def _calculate_letter_sequence(self, sequence): + + def _letters(x): + """ + Adapted from algorithm at http://en.wikipedia.org/wiki/Hexavigesimal + """ + v = '' + while x > 25: + r = x % 26 + 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) + + 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() + elif self.L1 == SESSION_SEQ_NUMBER: + return self._get_session_sequence_no() + elif self.L1 == STORED_SEQ_NUMBER: + return self._get_stored_sequence_no() + elif self.L1 == SEQUENCE_LETTER: + return self._get_sequence_letter() + + + #~ elif self.L1 == SUBFOLDER_SEQ_NUMBER: + #~ return self._getSubfolderSequenceNo() + + + + def _get_component(self): + #~ try: + if True: + if self.L0 == DATE_TIME: + return self._get_date_component() + elif self.L0 == TEXT: + return self.L1 + elif self.L0 == FILENAME: + return self._get_filename_component() + elif self.L0 == METADATA: + return self._get_metadata_component() + elif self.L0 == SEQUENCES: + return self._get_sequences_component() + elif self.L0 == JOB_CODE: + return self.rpd_file.job_code + elif self.L0 == SEPARATOR: + return os.sep + else: + # for development phase only + return '' + #~ 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 + + name = '' + + for self.L0, self.L1, self.L2 in self._get_values_from_pref_list(): + v = self._get_component() + if v: + name += v + + 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) + +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 + # 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) + +def get_video_metadata_component(video): + """ + Returns portion of video / subfolder name based on the metadata + + This is outside of a class definition because of the inheritence + hierarchy. + """ + + problem = None + if video.L1 == CODEC: + v = video.rpd_file.metadata.codec() + elif video.L1 == WIDTH: + v = video.rpd_file.metadata.width() + elif video.L1 == HEIGHT: + v = video.rpd_file.metadata.height() + elif video.L1 == FPS: + v = video.rpd_file.metadata.frames_per_second() + elif video.L1 == LENGTH: + v = video.rpd_file.metadata.length() + else: + raise TypeError("Invalid metadata option specified") + if video.L1 in [CODEC]: + if video.L2 == UPPERCASE: + v = v.upper() + elif video.L2 == LOWERCASE: + v = v.lower() + if not v: + video.rpd_file.add_problem(video.component, pn.MISSING_METADATA, _(video.L1)) + return v + +class Sequences: + """ + Holds sequence numbers and letters used in generating filenames. + """ + def __init__(self, downloads_today_tracker, stored_sequence_no): + self.session_sequence_no = 0 + self.sequence_letter = -1 + self.downloads_today_tracker = downloads_today_tracker + self.stored_sequence_no = stored_sequence_no + + def get_session_sequence_no(self): + return self.session_sequence_no + 1 + + 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): + v = self.downloads_today_tracker.get_downloads_today() + if v == -1: + return 1 + else: + return v + 1 + + 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 diff --git a/rapid/generatenameconfig.py b/rapid/generatenameconfig.py new file mode 100644 index 0000000..321761e --- /dev/null +++ b/rapid/generatenameconfig.py @@ -0,0 +1,482 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007, 2008, 2009, 2010, 2011 Damon Lynch + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +# Special key in each dictionary which specifies the order of elements. +# It is very important to have a consistent and rational order when displaying +# these prefs to the user, and dictionaries are unsorted. + +import os + +from gettext import gettext as _ + +ORDER_KEY = "__order__" + +# PLEASE NOTE: these values are duplicated in a dummy class whose function +# is to have them put into the translation template. If you change the values below +# then you MUST change the value in class i18TranslateMeThanks as well!! + +# *** Level 0 +DATE_TIME = 'Date time' +TEXT = 'Text' +FILENAME = 'Filename' +METADATA = 'Metadata' +SEQUENCES = 'Sequences' +JOB_CODE = 'Job code' + +SEPARATOR = os.sep + +# *** Level 1 + +# Date time +IMAGE_DATE = 'Image date' +TODAY = 'Today' +YESTERDAY = 'Yesterday' +VIDEO_DATE = 'Video date' +DOWNLOAD_TIME = 'Download time' + +# File name +NAME_EXTENSION = 'Name + extension' +NAME = 'Name' +EXTENSION = 'Extension' +IMAGE_NUMBER = 'Image number' +VIDEO_NUMBER = 'Video number' + +# Metadata +APERTURE = 'Aperture' +ISO = 'ISO' +EXPOSURE_TIME = 'Exposure time' +FOCAL_LENGTH = 'Focal length' +CAMERA_MAKE = 'Camera make' +CAMERA_MODEL = 'Camera model' +SHORT_CAMERA_MODEL = 'Short camera model' +SHORT_CAMERA_MODEL_HYPHEN = 'Hyphenated short camera model' +SERIAL_NUMBER = 'Serial number' +SHUTTER_COUNT = 'Shutter count' +OWNER_NAME = 'Owner name' + +# Video metadata +CODEC = 'Codec' +WIDTH = 'Width' +HEIGHT = 'Height' +FPS = 'Frames Per Second' +LENGTH = 'Length' + +#Image sequences +DOWNLOAD_SEQ_NUMBER = 'Downloads today' +SESSION_SEQ_NUMBER = 'Session number' +SUBFOLDER_SEQ_NUMBER = 'Subfolder number' +STORED_SEQ_NUMBER = 'Stored number' + +SEQUENCE_LETTER = 'Sequence letter' + + + +# *** Level 2 + +# Image number +IMAGE_NUMBER_ALL = 'All digits' +IMAGE_NUMBER_1 = 'Last digit' +IMAGE_NUMBER_2 = 'Last 2 digits' +IMAGE_NUMBER_3 = 'Last 3 digits' +IMAGE_NUMBER_4 = 'Last 4 digits' + + +# Case +ORIGINAL_CASE = "Original Case" +UPPERCASE = "UPPERCASE" +LOWERCASE = "lowercase" + +# Sequence number +SEQUENCE_NUMBER_1 = "One digit" +SEQUENCE_NUMBER_2 = "Two digits" +SEQUENCE_NUMBER_3 = "Three digits" +SEQUENCE_NUMBER_4 = "Four digits" +SEQUENCE_NUMBER_5 = "Five digits" +SEQUENCE_NUMBER_6 = "Six digits" +SEQUENCE_NUMBER_7 = "Seven digits" + + +# Now, define dictionaries and lists of valid combinations of preferences. + +# Level 2 + +# Date + +SUBSECONDS = 'Subseconds' + +# ****** NOTE 1: if changing LIST_DATE_TIME_L2, you MUST update the default subfolder preference below ***** +# ****** NOTE 2: if changing LIST_DATE_TIME_L2, you MUST update DATE_TIME_CONVERT below ***** +LIST_DATE_TIME_L2 = ['YYYYMMDD', 'YYYY-MM-DD','YYMMDD', 'YY-MM-DD', + 'MMDDYYYY', 'MMDDYY', 'MMDD', + 'DDMMYYYY', 'DDMMYY', 'YYYY', 'YY', + 'MM', 'DD', + 'HHMMSS', 'HHMM', 'HH-MM-SS', 'HH-MM', 'HH', 'MM (minutes)', 'SS'] + + +LIST_IMAGE_DATE_TIME_L2 = LIST_DATE_TIME_L2 + [SUBSECONDS] + +DEFAULT_SUBFOLDER_PREFS = [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[9], '/', '', '', DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0]] +DEFAULT_VIDEO_SUBFOLDER_PREFS = [DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[9], '/', '', '', DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0]] + +class i18TranslateMeThanks: + """ this class is never used in actual running code + It's purpose is to have these values inserted into the program's i18n template file + + """ + def __init__(self): + _('Date time') + _('Text') + _('Filename') + _('Metadata') + _('Sequences') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode + _('Job code') + _('Image date') + _('Video date') + _('Today') + _('Yesterday') + # Translators: Download time is the time and date that the download started (when the user clicked the Download button) + _('Download time') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Name + extension') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Name') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Extension') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Image number') + _('Video number') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Aperture') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('ISO') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Exposure time') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Focal length') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Camera make') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Camera model') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Short camera model') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Hyphenated short camera model') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Serial number') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Shutter count') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Owner name') + _('Codec') + _('Width') + _('Height') + _('Length') + _('Frames Per Second') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers + _('Downloads today') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers + _('Session number') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers + _('Subfolder number') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers + _('Stored number') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequenceletters + _('Sequence letter') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('All digits') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Last digit') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Last 2 digits') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Last 3 digits') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Last 4 digits') + # Translators: please not the capitalization of this text, and keep it the same if your language features capitalization + _("Original Case") + # Translators: please not the capitalization of this text, and keep it the same if your language features capitalization + _("UPPERCASE") + # Translators: please not the capitalization of this text, and keep it the same if your language features capitalization + _("lowercase") + _("One digit") + _("Two digits") + _("Three digits") + _("Four digits") + _("Five digits") + _("Six digits") + _("Seven digits") + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('Subseconds') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YYYYMMDD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YYYY-MM-DD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YYMMDD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YY-MM-DD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('MMDDYYYY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('MMDDYY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('MMDD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('DDMMYYYY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('DDMMYY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YYYY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('MM') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('DD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('HHMMSS') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('HHMM') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('HH-MM-SS') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('HH-MM') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('HH') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('MM (minutes)') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('SS') + + +# Convenience values for python datetime conversion using values in +# LIST_DATE_TIME_L2. Obviously the two must remain synchronized. + +DATE_TIME_CONVERT = ['%Y%m%d', '%Y-%m-%d','%y%m%d', '%y-%m-%d', + '%m%d%Y', '%m%d%y', '%m%d', + '%d%m%Y', '%d%m%y', '%Y', '%y', + '%m', '%d', + '%H%M%S', '%H%M', '%H-%M-%S', '%H-%M', + '%H', '%M', '%S'] + + +LIST_IMAGE_NUMBER_L2 = [IMAGE_NUMBER_ALL, IMAGE_NUMBER_1, IMAGE_NUMBER_2, + IMAGE_NUMBER_3, IMAGE_NUMBER_4] + + +LIST_CASE_L2 = [ORIGINAL_CASE, UPPERCASE, LOWERCASE] + +LIST_SEQUENCE_LETTER_L2 = [ + UPPERCASE, + LOWERCASE + ] + + + +LIST_SEQUENCE_NUMBERS_L2 = [ + SEQUENCE_NUMBER_1, + SEQUENCE_NUMBER_2, + SEQUENCE_NUMBER_3, + SEQUENCE_NUMBER_4, + SEQUENCE_NUMBER_5, + SEQUENCE_NUMBER_6, + SEQUENCE_NUMBER_7, + ] + + + +LIST_SHUTTER_COUNT_L2 = [ + SEQUENCE_NUMBER_3, + SEQUENCE_NUMBER_4, + SEQUENCE_NUMBER_5, + SEQUENCE_NUMBER_6, + ] + +# Level 1 +LIST_DATE_TIME_L1 = [IMAGE_DATE, TODAY, YESTERDAY, DOWNLOAD_TIME] +LIST_VIDEO_DATE_TIME_L1 = [VIDEO_DATE, TODAY, YESTERDAY, DOWNLOAD_TIME] + +DICT_DATE_TIME_L1 = { + IMAGE_DATE: LIST_IMAGE_DATE_TIME_L2, + TODAY: LIST_DATE_TIME_L2, + YESTERDAY: LIST_DATE_TIME_L2, + DOWNLOAD_TIME: LIST_DATE_TIME_L2, + ORDER_KEY: LIST_DATE_TIME_L1 + } + +VIDEO_DICT_DATE_TIME_L1 = { + VIDEO_DATE: LIST_IMAGE_DATE_TIME_L2, + TODAY: LIST_DATE_TIME_L2, + YESTERDAY: LIST_DATE_TIME_L2, + DOWNLOAD_TIME: LIST_DATE_TIME_L2, + ORDER_KEY: LIST_VIDEO_DATE_TIME_L1 + } + + +LIST_FILENAME_L1 = [NAME_EXTENSION, NAME, EXTENSION, IMAGE_NUMBER] + +DICT_FILENAME_L1 = { + NAME_EXTENSION: LIST_CASE_L2, + NAME: LIST_CASE_L2, + EXTENSION: LIST_CASE_L2, + IMAGE_NUMBER: LIST_IMAGE_NUMBER_L2, + ORDER_KEY: LIST_FILENAME_L1 + } + +LIST_VIDEO_FILENAME_L1 = [NAME_EXTENSION, NAME, EXTENSION, VIDEO_NUMBER] + +DICT_VIDEO_FILENAME_L1 = { + NAME_EXTENSION: LIST_CASE_L2, + NAME: LIST_CASE_L2, + EXTENSION: LIST_CASE_L2, + VIDEO_NUMBER: LIST_IMAGE_NUMBER_L2, + ORDER_KEY: LIST_VIDEO_FILENAME_L1 + } + + +LIST_SUBFOLDER_FILENAME_L1 = [EXTENSION] + +DICT_SUBFOLDER_FILENAME_L1 = { + EXTENSION: LIST_CASE_L2, + ORDER_KEY: LIST_SUBFOLDER_FILENAME_L1 +} + +LIST_METADATA_L1 = [APERTURE, ISO, EXPOSURE_TIME, FOCAL_LENGTH, + CAMERA_MAKE, CAMERA_MODEL, + SHORT_CAMERA_MODEL, + SHORT_CAMERA_MODEL_HYPHEN, + SERIAL_NUMBER, + SHUTTER_COUNT, + OWNER_NAME] + +LIST_VIDEO_METADATA_L1 = [CODEC, WIDTH, HEIGHT, LENGTH, FPS] + +DICT_METADATA_L1 = { + APERTURE: None, + ISO: None, + EXPOSURE_TIME: None, + FOCAL_LENGTH: None, + CAMERA_MAKE: LIST_CASE_L2, + CAMERA_MODEL: LIST_CASE_L2, + SHORT_CAMERA_MODEL: LIST_CASE_L2, + SHORT_CAMERA_MODEL_HYPHEN: LIST_CASE_L2, + SERIAL_NUMBER: None, + SHUTTER_COUNT: LIST_SHUTTER_COUNT_L2, + OWNER_NAME: LIST_CASE_L2, + ORDER_KEY: LIST_METADATA_L1 + } + +DICT_VIDEO_METADATA_L1 = { + CODEC: LIST_CASE_L2, + WIDTH: None, + HEIGHT: None, + LENGTH: None, + FPS: None, + ORDER_KEY: LIST_VIDEO_METADATA_L1 + } + +LIST_SEQUENCE_L1 = [ + DOWNLOAD_SEQ_NUMBER, + STORED_SEQ_NUMBER, + SESSION_SEQ_NUMBER, + SEQUENCE_LETTER + ] + +DICT_SEQUENCE_L1 = { + DOWNLOAD_SEQ_NUMBER: LIST_SEQUENCE_NUMBERS_L2, + STORED_SEQ_NUMBER: LIST_SEQUENCE_NUMBERS_L2, + SESSION_SEQ_NUMBER: LIST_SEQUENCE_NUMBERS_L2, + SEQUENCE_LETTER: LIST_SEQUENCE_LETTER_L2, + ORDER_KEY: LIST_SEQUENCE_L1 + } + + +# Level 0 + + +LIST_IMAGE_RENAME_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, + SEQUENCES, JOB_CODE] + +LIST_VIDEO_RENAME_L0 = LIST_IMAGE_RENAME_L0 + + +DICT_IMAGE_RENAME_L0 = { + DATE_TIME: DICT_DATE_TIME_L1, + TEXT: None, + FILENAME: DICT_FILENAME_L1, + METADATA: DICT_METADATA_L1, + SEQUENCES: DICT_SEQUENCE_L1, + JOB_CODE: None, + ORDER_KEY: LIST_IMAGE_RENAME_L0 + } + +DICT_VIDEO_RENAME_L0 = { + DATE_TIME: VIDEO_DICT_DATE_TIME_L1, + TEXT: None, + FILENAME: DICT_VIDEO_FILENAME_L1, + METADATA: DICT_VIDEO_METADATA_L1, + SEQUENCES: DICT_SEQUENCE_L1, + JOB_CODE: None, + ORDER_KEY: LIST_VIDEO_RENAME_L0 + } + +LIST_SUBFOLDER_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, JOB_CODE, SEPARATOR] + +DICT_SUBFOLDER_L0 = { + DATE_TIME: DICT_DATE_TIME_L1, + TEXT: None, + FILENAME: DICT_SUBFOLDER_FILENAME_L1, + METADATA: DICT_METADATA_L1, + JOB_CODE: None, + SEPARATOR: None, + ORDER_KEY: LIST_SUBFOLDER_L0 + } + +LIST_VIDEO_SUBFOLDER_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, JOB_CODE, SEPARATOR] + +DICT_VIDEO_SUBFOLDER_L0 = { + DATE_TIME: VIDEO_DICT_DATE_TIME_L1, + TEXT: None, + FILENAME: DICT_SUBFOLDER_FILENAME_L1, + METADATA: DICT_VIDEO_METADATA_L1, + JOB_CODE: None, + SEPARATOR: None, + ORDER_KEY: LIST_VIDEO_SUBFOLDER_L0 + } + +# preference elements that require metadata +# note there is no need to specify lower level elements if a higher level +# element is necessary for them to be present to begin with +METADATA_ELEMENTS = [METADATA, IMAGE_DATE] + +# preference elements that are sequence numbers or letters +SEQUENCE_ELEMENTS = [ + DOWNLOAD_SEQ_NUMBER, + SESSION_SEQ_NUMBER, + SUBFOLDER_SEQ_NUMBER, + STORED_SEQ_NUMBER, + SEQUENCE_LETTER] + +# preference elements that do not require metadata and are not fixed +# as above, there is no need to specify lower level elements if a higher level +# element is necessary for them to be present to begin with +DYNAMIC_NON_METADATA_ELEMENTS = [ + TODAY, YESTERDAY, + FILENAME] + SEQUENCE_ELEMENTS diff --git a/rapid/glade3/about.ui b/rapid/glade3/about.ui new file mode 100644 index 0000000..ef8a65b --- /dev/null +++ b/rapid/glade3/about.ui @@ -0,0 +1,75 @@ + + + + + + False + 5 + True + rapid-photo-downloader.svg + dialog + Rapid Photo Downloader + Copyright Damon Lynch 2007-11 + Import your photos and videos efficiently and reliably + http://www.damonlynch.net/rapid + Rapid Photo Downloader is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. + +Rapid Photo Downloader is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with Rapid Photo Downloader; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + Damon Lynch <damonlynch@gmail.com> + Anton Alyab'ev <subeditor@dolgopa.org> +Lőrincz András <level.andrasnak@gmail.com> +Michel Ange <michelange@wanadoo.fr> +Alain J. Baudrez <a.baudrez@gmail.com> +Bert <crinbert@yahoo.com> +Martin Dahl Moe +Martin Egger <martin.egger@gmx.net> +Miroslav Matejaš <silverspace@ubuntu-hr.org> +Nicolás M. Zahlut <nzahlut@live.com> +Erik M +Jose Luis Navarro <jlnavarro111@gmail.com> +Tomas Novak <kuvaly@seznam.cz> +Abel O'Rian <abel.orian@gmail.com> +Balazs Oveges <ovegesb@freemail.hu> +Daniel Paessler <daniel@paessler.org> +Miloš Popović <gpopac@gmail.com> +Michal Predotka <mpredotka@googlemail.com> +Ye Qing <allen19920930@gmail.com> +Luca Reverberi <thereve@gmail.com> +Mikko Ruohola <polarfox@polarfox.net> +Sergiy Gavrylov <sergiovana@bigmir.net> +Sergei Sedov <sedov@webmail.perm.ru> +Marco Solari <marcosolari@gmail.com> +Toni Lähdekorpi <toni@lygon.net> +Ulf Urdén <ulf.urden@purplescout.com> +Julien Valroff <julien@kirya.net> +Aron Xu <happyaron.xu@gmail.com> +梁其学 <yalongbay@gmail.com> + rapid-photo-downloader.svg + True + + + True + False + 2 + + + True + False + end + + + False + True + end + 0 + + + + + + + + + diff --git a/rapid/glade3/photo.png b/rapid/glade3/photo.png deleted file mode 100644 index b8bd550..0000000 Binary files a/rapid/glade3/photo.png and /dev/null differ diff --git a/rapid/glade3/photo.svg b/rapid/glade3/photo.svg new file mode 100644 index 0000000..95c57d0 --- /dev/null +++ b/rapid/glade3/photo.svg @@ -0,0 +1,1208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rapid/glade3/photo24.png b/rapid/glade3/photo24.png deleted file mode 100644 index 53b2271..0000000 Binary files a/rapid/glade3/photo24.png and /dev/null differ diff --git a/rapid/glade3/photo66.png b/rapid/glade3/photo66.png new file mode 100644 index 0000000..1bef29f Binary files /dev/null and b/rapid/glade3/photo66.png differ diff --git a/rapid/glade3/photo_icon.png b/rapid/glade3/photo_icon.png new file mode 100644 index 0000000..52e22bc Binary files /dev/null and b/rapid/glade3/photo_icon.png differ diff --git a/rapid/glade3/photo_small.png b/rapid/glade3/photo_small.png deleted file mode 100644 index f44d380..0000000 Binary files a/rapid/glade3/photo_small.png and /dev/null differ diff --git a/rapid/glade3/photo_small_shadow.png b/rapid/glade3/photo_small_shadow.png deleted file mode 100644 index fe85cd9..0000000 Binary files a/rapid/glade3/photo_small_shadow.png and /dev/null differ diff --git a/rapid/glade3/prefs.ui b/rapid/glade3/prefs.ui new file mode 100644 index 0000000..ddbacdf --- /dev/null +++ b/rapid/glade3/prefs.ui @@ -0,0 +1,3284 @@ + + + + + + 23 + 1 + 10 + + + 59 + 1 + 10 + + + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 5 + Preferences: Rapid Photo Downloader + True + center-on-parent + 500 + rapid-photo-downloader.svg + dialog + + + + + True + False + 2 + + + True + False + end + + + gtk-help + True + True + True + False + True + + + False + False + 0 + True + + + + + gtk-close + True + True + True + False + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 2 + + + True + True + queue + automatic + automatic + in + + + 100 + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + + + + + + True + True + 5 + 0 + + + + + True + True + False + + + True + False + 12 + + + True + False + + + True + False + 6 + + + True + False + gtk-directory + + + False + True + 0 + + + + + True + False + <span weight="bold" size="x-large">Photo Download Folders</span> + True + + + False + False + 1 + + + + + True + True + 0 + + + + + True + False + + + False + True + 1 + + + + + False + False + 0 + + + + + True + False + 12 + + + True + False + + + False + True + 0 + + + + + True + False + 7 + 3 + + + True + False + 0 + <i>Example: /home/user/Pictures</i> + True + True + + + 1 + 3 + 5 + 6 + + + + + True + False + 0 + <b>Download Subfolders</b> + True + + + 3 + 3 + 4 + + + + + + True + False + 0 + Download folder: + + + 1 + 2 + 2 + 3 + + + + + + + True + False + 0 + 12 + Choose the download folder. Subfolders for the downloaded photos will be automatically created in this folder using the structure specified below. + True + True + + + 1 + 3 + 1 + 2 + + + + + + True + False + 0 + <b>Download Folder</b> + True + + + 3 + + + + + + True + False + 0 + 0 + True + word-char + + + 1 + 3 + 6 + 7 + + + + + + True + False + + + + + + 1 + 3 + 4 + 5 + 12 + + + + + + + + + + + + + + + + + + + + + + + True + True + 1 + + + + + True + False + + + False + True + 2 + + + + + True + True + 12 + 1 + + + + + Download Folders + + + + + True + False + Photo Folders + + + False + + + + + True + False + 12 + + + True + False + + + True + False + 6 + + + True + False + gtk-convert + + + False + True + 0 + + + + + True + False + <span weight="bold" size="x-large">Photo Rename</span> + True + + + False + True + 1 + + + + + True + True + 0 + + + + + True + False + + + False + True + 1 + + + + + False + False + 0 + + + + + True + False + 12 + + + True + False + + + False + True + 0 + + + + + True + False + 12 + + + False + 0 + <b>Photo Rename</b> + True + + + False + True + 0 + + + + + True + True + automatic + automatic + True + + + True + False + queue + none + + + True + False + + + + + + + + + + True + True + 1 + + + + + True + False + 3 + 3 + + + True + False + + + + 2 + 3 + + + + + + True + False + + + + 1 + 2 + + + + + + + True + False + 0 + 0 + translators please ignore this + True + + + 2 + 3 + 2 + 3 + GTK_FILL + + + + + True + False + 0 + translators please ignore this + + + 2 + 3 + 1 + 2 + + + + + + True + False + 0 + 0 + <i>New:</i> + True + + + 1 + 2 + 2 + 3 + GTK_FILL + GTK_FILL + + + + + True + False + 0 + <i>Original:</i> + True + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + + True + False + 0 + <b>Example</b> + True + + + 3 + 12 + + + + + False + False + 2 + + + + + True + True + 1 + + + + + True + False + + + False + True + 2 + + + + + True + True + 12 + 1 + + + + + Rename + 1 + + + + + True + False + Photo Rename + + + 1 + False + + + + + True + False + 12 + + + True + False + + + True + False + 6 + + + True + False + gtk-directory + + + False + True + 0 + + + + + True + False + <span weight="bold" size="x-large">Video Download Folders</span> + True + + + False + False + 1 + + + + + True + True + 0 + + + + + True + False + + + False + True + 1 + + + + + False + False + 0 + + + + + False + + + False + 0 + 12 + 10 + Sorry, video downloading functionality disabled. To download videos, please install the <i>hachoir metadata</i> and <i>kaa metadata</i> packages for python. + True + True + + + True + True + 0 + + + + + False + False + 1 + + + + + True + False + 12 + + + True + False + + + False + True + 0 + + + + + True + False + 7 + 3 + + + True + False + 0 + Download folder: + + + 1 + 2 + 2 + 3 + + + + + + + True + False + 0 + 12 + Choose the download folder. Subfolders for the downloaded videos will be automatically created in this folder using the structure specified below. + True + True + + + 1 + 3 + 1 + 2 + + + + + + True + False + 0 + <b>Download Folder</b> + True + + + 3 + + + + + + True + False + 0 + <b>Download Subfolders</b> + True + + + 3 + 3 + 4 + + + + + + True + False + 0 + <i>Example: /home/user/Pictures</i> + True + True + + + 1 + 3 + 5 + 6 + + + + + True + False + 0 + 0 + True + word-char + + + 1 + 3 + 6 + 7 + + + + + + True + False + + + + + + 1 + 3 + 4 + 5 + 12 + + + + + + + + + + + + + + + + + + + + + + + True + True + 1 + + + + + True + False + + + False + True + 2 + + + + + True + True + 12 + 2 + + + + + 2 + + + + + True + False + Video Folders + + + 2 + False + + + + + True + False + 12 + + + True + False + + + True + False + 6 + + + True + False + gtk-convert + + + False + True + 0 + + + + + True + False + <span weight="bold" size="x-large">Video Rename</span> + True + + + False + True + 1 + + + + + True + True + 0 + + + + + True + False + + + False + True + 1 + + + + + False + False + 0 + + + + + True + False + 12 + + + True + False + + + False + True + 0 + + + + + True + False + 12 + + + False + 0 + Sorry, video downloading functionality disabled. To download videos, please install the <i>hachoir metadata</i> and <i>kaa metadata</i> packages for python. + True + True + + + False + False + 0 + + + + + True + True + automatic + automatic + True + + + True + False + queue + none + + + True + False + + + + + + + + + + True + True + 1 + + + + + True + False + 3 + 3 + + + True + False + + + + 2 + 3 + + + + + + True + False + + + + 1 + 2 + + + + + + + True + False + 0 + 0 + translators please ignore this + True + + + 2 + 3 + 2 + 3 + GTK_FILL + + + + + True + False + 0 + translators please ignore this + + + 2 + 3 + 1 + 2 + + + + + + True + False + 0 + 0 + <i>New:</i> + True + + + 1 + 2 + 2 + 3 + GTK_FILL + GTK_FILL + + + + + True + False + 0 + <i>Original:</i> + True + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + + True + False + 0 + <b>Example</b> + True + + + 3 + 12 + + + + + False + False + 2 + + + + + True + True + 1 + + + + + True + False + + + False + True + 2 + + + + + True + True + 12 + 1 + + + + + 3 + + + + + True + False + Video Rename + + + 3 + False + + + + + True + False + 12 + + + True + False + + + True + False + 6 + + + True + False + input-keyboard + + + False + True + 0 + + + + + True + False + <span weight="bold" size="x-large">Rename Options</span> + True + + + False + True + 1 + + + + + True + True + 0 + + + + + True + False + + + False + True + 1 + + + + + False + False + 0 + + + + + True + False + + + True + False + 0 + 12 + <b>Sequence Numbers</b> + True + + + False + True + 0 + + + + + True + False + 12 + + + True + False + 12 + + + False + True + 0 + + + + + True + False + 12 + + + True + False + 0 + Specify the time in 24 hour format at which the <i>Downloads today</i> sequence number should be reset. + True + True + + + True + True + 0 + + + + + True + False + + + True + False + 6 + + + True + False + 0 + Day start: + + + True + True + 0 + + + + + True + False + 0 + Downloads today: + + + True + True + 1 + + + + + True + False + 0 + Stored number: + + + True + True + 2 + + + + + False + True + 0 + + + + + True + False + 6 + + + + False + True + 1 + + + + + True + False + 6 + + + True + False + + + True + True + 2 + + 2 + 1 + True + hour_adjustment + True + + + + False + False + 0 + + + + + True + False + : + + + False + False + 1 + + + + + True + True + 2 + + 2 + 1 + True + minute_adjustment + True + + + + False + False + 2 + + + + + True + False + 0 + hh:mm + + + False + True + 3 + + + + + False + True + 0 + + + + + + + + + + + False + True + 2 + + + + + True + True + 1 + + + + + Synchronize RAW + JPEG sequence numbers + True + True + False + False + True + + + + True + True + 2 + + + + + True + True + 1 + + + + + True + False + + + False + True + 2 + + + + + False + True + 12 + 1 + + + + + True + False + 0 + 12 + <b>Compatibility with Other Operating Systems</b> + True + + + False + True + 3 + + + + + True + False + 12 + + + True + False + 12 + + + False + True + 0 + + + + + True + False + 2 + 2 + + + True + False + 0 + Specify whether photo, video and folder names should have any characters removed that are not allowed by other operating systems. + True + + + 2 + + + + + Strip incompatible characters + True + True + False + False + True + True + + + + 2 + 1 + 2 + + + + + True + True + 1 + + + + + True + False + + + False + True + 2 + + + + + False + True + 12 + 4 + + + + + True + True + 12 + 1 + + + + + 4 + + + + + True + False + Rename Options + + + 4 + False + + + + + True + False + 12 + + + True + False + + + True + False + 6 + + + True + False + rapid-photo-downloader-jobcode + + + False + True + 0 + + + + + True + False + <span weight="bold" size="x-large">Job Codes</span> + True + + + False + True + 1 + + + + + True + True + 0 + + + + + True + False + + + False + True + 1 + + + + + False + False + 0 + + + + + True + False + + + True + False + + + False + 0 + 12 + <b>Job Codes</b> + True + + + False + True + 0 + + + + + True + False + 12 + + + True + False + + + False + True + 0 + + + + + True + True + automatic + automatic + in + + + 250 + True + True + False + True + + + + + False + True + 1 + + + + + True + False + + + True + False + 12 + start + + + _Add... + True + True + True + False + True + + + + False + False + 0 + + + + + gtk-remove + True + True + True + False + True + + + + False + False + 1 + + + + + R_emove All + True + True + True + False + True + + + + False + False + 2 + + + + + True + True + 0 + + + + + False + True + 2 + + + + + True + True + 1 + + + + + + + + True + True + 0 + + + + + True + True + 1 + + + + + 5 + + + + + True + False + Job Codes + + + 5 + False + + + + + True + False + 12 + + + True + False + + + True + False + 6 + + + True + False + media-flash + + + False + True + 0 + + + + + True + False + <span weight="bold" size="x-large">Devices</span> + True + + + False + True + 1 + + + + + True + True + 0 + + + + + True + False + + + False + True + 1 + + + + + False + False + 0 + + + + + True + False + 12 + + + False + 0 + 12 + <b>Devices</b> + True + + + False + True + 0 + + + + + True + False + 0 + 12 + Devices are from where to download photos and videos, such as cameras, memory cards or Portable Storage Devices. + +You can download photos from multiple devices simultaneously, or you can specify a location on your hard drive. + +<i>If downloading directly from your camera works poorly or not at all, try setting it to PTP mode. If that is not possible, consider using a card reader.</i> + True + True + + + True + True + 1 + + + + + True + False + + + True + False + 3 + + + False + True + 0 + + + + + True + False + 3 + 2 + 3 + + + Automatically detect Portable Storage Devices + True + True + False + False + True + + + + 1 + 2 + 1 + 2 + + + + + Automatically detect devices + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + True + + + + 2 + + + + + True + False + 0 + 6 + If you enable automatic detection of Portable Storage Devices, the entire device will be scanned for images. On large devices, this could take some time. + True + + + 1 + 2 + 2 + 3 + + + + + + + + + + + True + True + 1 + + + + + True + False + + + False + True + 2 + + + + + False + True + 2 + + + + + True + False + 12 + + + True + False + 2 + 2 + + + True + False + 0 + 12 + Location: + + + 1 + 2 + GTK_FILL + GTK_SHRINK + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 0 + 12 + 12 + If you disable automatic detection, choose the exact location of the images and videos. + True + + + 2 + + + + + + + + True + True + 0 + + + + + + + + True + False + + + False + True + 2 + + + + + False + True + 3 + + + + + False + True + 12 + 1 + + + + + 6 + + + + + True + False + Devices + + + 6 + False + + + + + True + False + + + True + False + + + True + False + 6 + + + True + False + drive-removable-media + + + False + True + 0 + + + + + True + False + <span weight="bold" size="x-large">Backup</span> + True + + + False + True + 1 + + + + + True + True + 0 + + + + + True + False + + + False + True + 1 + + + + + False + False + 0 + + + + + True + False + + + False + 0 + 12 + <b>Backup</b> + True + + + False + True + 0 + + + + + True + False + 12 + + + True + False + + + False + True + 0 + + + + + True + False + 9 + 4 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 0 + 12 + If you disable automatic detection, choose the exact backup location. + True + + + 1 + 4 + 7 + 8 + + + + + Automatically detect backup devices + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + True + + + + 1 + 4 + 2 + 3 + GTK_FILL + + + + + True + False + 0 + 12 + You can have your photos and videos backed up to multiple locations as they are downloaded, e.g. external hard drives. + True + + + 4 + + + + + + Backup photos and videos when downloading + True + True + False + False + True + True + + + + 4 + 1 + 2 + + + + + + True + False + 0 + 6 + Specify the folder in which backups are stored on the device. + +<i>Note: this will also be used to determine whether or not the device is used for backups. For each device you wish to use for backing up to, create a folder in it with one of these names.</i> + True + True + + + 2 + 4 + 3 + 4 + + + + + True + False + 0 + Backup location: + + + 1 + 3 + 8 + 9 + GTK_FILL + + + + + + True + False + 0 + Photo backup folder name: + + + 2 + 3 + 6 + 5 + GTK_FILL + + 6 + + + + + True + False + 0 + 0 + 6 + <i>Example:</i> + True + + + 2 + 3 + 6 + 7 + GTK_FILL + + + + + True + False + 0 + 0 + 6 + <i>/media/externaldrive/Photos</i> + True + + + 3 + 4 + 6 + 7 + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + + + 3 + 4 + 4 + 5 + + 6 + + + + + True + False + 0 + Video backup folder name: + + + 2 + 3 + 5 + 6 + GTK_FILL + + 6 + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + + + 3 + 4 + 5 + 6 + + 6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + 1 + + + + + True + False + + + False + True + 2 + + + + + False + True + 1 + + + + + True + True + 12 + 1 + + + + + 7 + + + + + True + False + Backup + + + 7 + False + + + + + True + False + + + True + False + + + True + False + 6 + + + True + False + gtk-execute + + + False + True + 0 + + + + + True + False + <span weight="bold" size="x-large">Miscellaneous</span> + True + + + False + True + 1 + + + + + True + True + 0 + + + + + True + False + + + False + True + 1 + + + + + False + False + 0 + + + + + True + False + + + True + False + 0 + 12 + <b>Program Automation</b> + True + + + False + True + 0 + + + + + True + False + 6 + + + False + + + False + True + 0 + + + + + True + False + 7 + 3 + + + Unmount ("eject") device upon download completion + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + True + + + + 3 + 2 + 3 + + + + + Start downloading at program startup + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + True + + + + 3 + + + + + Start downloading upon device insertion + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + True + + + + 3 + 1 + 2 + + + + + Exit program when download completes + True + True + False + False + True + + + + 3 + 3 + 4 + + + + + Delete photos and videos from device upon download completion + True + True + False + False + True + + + + 3 + 5 + 6 + + + + + Exit program even if download had warnings or errors + True + True + False + False + True + + + + 1 + 2 + 4 + 5 + + + + + + + + + + + + + + + + + + + + True + True + 24 + 1 + + + + + True + False + + + False + True + 2 + + + + + False + True + 12 + 1 + + + + + + + + True + False + 6 + + + False + + + False + True + 0 + + + + + True + False + 3 + + + + + + + + + + + + True + True + 24 + 1 + + + + + True + False + + + False + True + 2 + + + + + False + True + 12 + 3 + + + + + True + True + 12 + 1 + + + + + 8 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Miscillaneous + + + 8 + False + + + + + True + False + 12 + + + True + False + + + True + False + 6 + + + True + False + gtk-dialog-error + + + False + True + 0 + + + + + True + False + <span weight="bold" size="x-large">Error Handling</span> + True + + + False + True + 1 + + + + + True + True + 0 + + + + + True + False + + + False + True + 1 + + + + + False + False + 0 + + + + + True + False + 12 + + + True + False + + + False + True + 0 + + + + + True + False + 14 + 2 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + + 2 + 3 + GTK_FILL + GTK_FILL + + + + + True + False + 0 + <b>Photo and Video Name Conflicts</b> + True + + + 2 + GTK_FILL + + + + + Add unique identifier + True + True + False + False + True + True + True + + + + 1 + 2 + 3 + 4 + GTK_FILL + + + + + Skip download + True + True + False + False + True + True + add_identifier_radiobutton + + + 1 + 2 + 2 + 3 + GTK_FILL + + + + + True + False + 0 + 12 + When a photo or video of the same name has already been downloaded, choose whether to skip downloading the file, or to add a unique indentifier. + True + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + True + False + 0 + 12 + When backing up, choose whether to overwrite a file on the backup device that has the same name, or skip backing it up. + True + + + 1 + 2 + 4 + 5 + GTK_FILL + + + + + Overwrite + True + True + False + False + True + backup_duplicate_skip_radiobutton + + + + 1 + 2 + 5 + 6 + + + + + Skip + True + True + False + False + True + True + + + + 1 + 2 + 6 + 7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + 1 + + + + + True + False + + + False + True + 2 + + + + + False + True + 12 + 1 + + + + + 9 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Error Handling + + + 9 + False + + + + + True + True + 6 + 1 + + + + + True + True + 5 + 1 + + + + + + + + + help_button1 + close_button + + + diff --git a/rapid/glade3/rapid.glade b/rapid/glade3/rapid.glade deleted file mode 100644 index 163174b..0000000 --- a/rapid/glade3/rapid.glade +++ /dev/null @@ -1,3559 +0,0 @@ - - - - - - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 5 - Preferences: Rapid Photo Downloader - True - center-on-parent - 500 - rapid-photo-downloader.svg - dialog - - - - - True - 2 - - - True - 2 - - - True - True - queue - automatic - automatic - in - - - 100 - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - False - - - - - - 5 - 0 - - - - - True - True - False - - - True - 12 - - - True - - - True - 6 - - - True - gtk-directory - - - False - 0 - - - - - True - <span weight="bold" size="x-large">Photo Download Folders</span> - True - - - False - False - 1 - - - - - 0 - - - - - True - - - False - 1 - - - - - False - False - 0 - - - - - True - 12 - - - True - - - False - 0 - - - - - True - 7 - 3 - - - True - 0 - <i>Example: /home/user/Pictures</i> - True - True - - - 1 - 3 - 5 - 6 - - - - - True - 0 - <b>Download Subfolders</b> - True - - - 3 - 3 - 4 - - - - - - True - 0 - Download folder: - - - 1 - 2 - 2 - 3 - - - - - - - True - 0 - 12 - Choose the download folder. Subfolders for the downloaded photos will be automatically created in this folder using the structure specified below. - True - True - - - 1 - 3 - 1 - 2 - - - - - - True - 0 - <b>Download Folder</b> - True - - - 3 - - - - - - True - 0 - 0 - True - word-char - - - 1 - 3 - 6 - 7 - - - - - - True - - - - - - 1 - 3 - 4 - 5 - 12 - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - True - - - False - 2 - - - - - 12 - 1 - - - - - Download Folders - - - - - True - Photo Folders - - - False - tab - - - - - True - 12 - - - True - - - True - 6 - - - True - gtk-convert - - - False - 0 - - - - - True - <span weight="bold" size="x-large">Photo Rename</span> - True - - - False - 1 - - - - - 0 - - - - - True - - - False - 1 - - - - - False - False - 0 - - - - - True - 12 - - - True - - - False - 0 - - - - - True - 12 - - - 0 - <b>Photo Rename</b> - True - - - False - 0 - - - - - True - True - automatic - automatic - True - - - True - queue - none - - - True - - - - - - - - - - 1 - - - - - True - 3 - 3 - - - True - - - - 2 - 3 - - - - - - True - - - - 1 - 2 - - - - - - - True - 0 - 0 - translators please ignore this - True - - - 2 - 3 - 2 - 3 - GTK_FILL - - - - - True - 0 - translators please ignore this - - - 2 - 3 - 1 - 2 - - - - - - True - 0 - 0 - <i>New:</i> - True - - - 1 - 2 - 2 - 3 - GTK_FILL - GTK_FILL - - - - - True - 0 - <i>Original:</i> - True - - - 1 - 2 - 1 - 2 - GTK_FILL - - - - - - True - 0 - <b>Example</b> - True - - - 3 - 12 - - - - - False - False - 2 - - - - - 1 - - - - - True - - - False - 2 - - - - - 12 - 1 - - - - - Rename - 1 - - - - - True - Photo Rename - - - 1 - False - tab - - - - - True - 12 - - - True - - - True - 6 - - - True - gtk-directory - - - False - 0 - - - - - True - <span weight="bold" size="x-large">Video Download Folders</span> - True - - - False - False - 1 - - - - - 0 - - - - - True - - - False - 1 - - - - - False - False - 0 - - - - - - - 0 - 12 - 10 - Sorry, video downloading functionality disabled. To download videos, please install the <i>hachoir metadata</i> and <i>kaa metadata</i> packages for python. - True - True - - - 0 - - - - - False - False - 1 - - - - - True - 12 - - - True - - - False - 0 - - - - - True - 7 - 3 - - - True - 0 - Download folder: - - - 1 - 2 - 2 - 3 - - - - - - - True - 0 - 12 - Choose the download folder. Subfolders for the downloaded videos will be automatically created in this folder using the structure specified below. - True - True - - - 1 - 3 - 1 - 2 - - - - - - True - 0 - <b>Download Folder</b> - True - - - 3 - - - - - - True - 0 - <b>Download Subfolders</b> - True - - - 3 - 3 - 4 - - - - - - True - 0 - <i>Example: /home/user/Pictures</i> - True - True - - - 1 - 3 - 5 - 6 - - - - - True - 0 - 0 - True - word-char - - - 1 - 3 - 6 - 7 - - - - - - True - - - - - - 1 - 3 - 4 - 5 - 12 - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - True - - - False - 2 - - - - - 12 - 2 - - - - - 2 - - - - - True - Video Folders - - - 2 - False - tab - - - - - True - 12 - - - True - - - True - 6 - - - True - gtk-convert - - - False - 0 - - - - - True - <span weight="bold" size="x-large">Video Rename</span> - True - - - False - 1 - - - - - 0 - - - - - True - - - False - 1 - - - - - False - False - 0 - - - - - True - 12 - - - True - - - False - 0 - - - - - True - 12 - - - 0 - Sorry, video downloading functionality disabled. To download videos, please install the <i>hachoir metadata</i> and <i>kaa metadata</i> packages for python. - True - True - - - False - False - 0 - - - - - True - True - automatic - automatic - True - - - True - queue - none - - - True - - - - - - - - - - 1 - - - - - True - 3 - 3 - - - True - - - - 2 - 3 - - - - - - True - - - - 1 - 2 - - - - - - - True - 0 - 0 - translators please ignore this - True - - - 2 - 3 - 2 - 3 - GTK_FILL - - - - - True - 0 - translators please ignore this - - - 2 - 3 - 1 - 2 - - - - - - True - 0 - 0 - <i>New:</i> - True - - - 1 - 2 - 2 - 3 - GTK_FILL - GTK_FILL - - - - - True - 0 - <i>Original:</i> - True - - - 1 - 2 - 1 - 2 - GTK_FILL - - - - - - True - 0 - <b>Example</b> - True - - - 3 - 12 - - - - - False - False - 2 - - - - - 1 - - - - - True - - - False - 2 - - - - - 12 - 1 - - - - - 3 - - - - - True - Video Rename - - - 3 - False - tab - - - - - True - 12 - - - True - - - True - 6 - - - True - input-keyboard - - - False - 0 - - - - - True - <span weight="bold" size="x-large">Rename Options</span> - True - - - False - 1 - - - - - 0 - - - - - True - - - False - 1 - - - - - False - False - 0 - - - - - True - - - True - 0 - 12 - <b>Sequence Numbers</b> - True - - - False - 0 - - - - - True - 12 - - - True - 12 - - - False - 0 - - - - - True - 12 - - - True - 0 - Specify the time in 24 hour format at which the <i>Downloads today</i> sequence number should be reset. - True - True - - - 0 - - - - - True - - - True - 6 - - - True - 0 - Day start: - - - 0 - - - - - True - 0 - Downloads today: - - - 1 - - - - - True - 0 - Stored number: - - - 2 - - - - - False - 0 - - - - - True - 6 - - - - False - 1 - - - - - True - 6 - - - True - - - True - True - 2 - 1 - True - 0 0 23 1 10 0 - True - - - - False - False - 0 - - - - - True - : - - - False - False - 1 - - - - - True - True - 2 - 1 - True - 0 0 59 1 10 0 - True - - - - False - False - 2 - - - - - True - 0 - hh:mm - - - False - 3 - - - - - False - 0 - - - - - - - - - - - False - 2 - - - - - 1 - - - - - Synchronize RAW + JPEG sequence numbers - True - True - False - True - - - - 2 - - - - - 1 - - - - - True - - - False - 2 - - - - - False - 12 - 1 - - - - - True - 0 - 12 - <b>Compatibility with Other Operating Systems</b> - True - - - False - 3 - - - - - True - 12 - - - True - 12 - - - False - 0 - - - - - True - 2 - 2 - - - True - 0 - Specify whether photo, video and folder names should have any characters removed that are not allowed by other operating systems. - True - - - 2 - - - - - Strip incompatible characters - True - True - False - True - True - - - - 2 - 1 - 2 - - - - - 1 - - - - - True - - - False - 2 - - - - - False - 12 - 4 - - - - - 12 - 1 - - - - - 4 - - - - - True - Rename Options - - - 4 - False - tab - - - - - True - 12 - - - True - - - True - 6 - - - True - rapid-photo-downloader-jobcode - - - False - 0 - - - - - True - <span weight="bold" size="x-large">Job Codes</span> - True - - - False - 1 - - - - - 0 - - - - - True - - - False - 1 - - - - - False - False - 0 - - - - - True - - - True - - - 0 - 12 - <b>Job Codes</b> - True - - - False - 0 - - - - - True - 12 - - - True - - - False - 0 - - - - - True - True - automatic - automatic - in - - - 250 - True - True - False - True - - - - - False - 1 - - - - - True - - - True - 12 - start - - - _Add... - True - True - True - True - - - - False - False - 0 - - - - - gtk-remove - True - True - True - True - - - - False - False - 1 - - - - - R_emove All - True - True - True - True - - - - False - False - 2 - - - - - 0 - - - - - False - 2 - - - - - 1 - - - - - - - - 0 - - - - - 1 - - - - - 5 - - - - - True - Job Codes - - - 5 - False - tab - - - - - True - 12 - - - True - - - True - 6 - - - True - media-flash - - - False - 0 - - - - - True - <span weight="bold" size="x-large">Devices</span> - True - - - False - 1 - - - - - 0 - - - - - True - - - False - 1 - - - - - False - False - 0 - - - - - True - 12 - - - 0 - 12 - <b>Devices</b> - True - - - False - 0 - - - - - True - 0 - 12 - Devices are from where to download photos and videos, such as cameras, memory cards or Portable Storage Devices. - -You can download photos from multiple devices simultaneously, or you can specify a location on your hard drive. - -<i>If downloading directly from your camera works poorly or not at all, try setting it to PTP mode. If that is not possible, consider using a card reader.</i> - True - True - - - 1 - - - - - True - - - True - 3 - - - False - 0 - - - - - True - 3 - 2 - 3 - - - Automatically detect Portable Storage Devices - True - True - False - True - - - - 1 - 2 - 1 - 2 - - - - - Automatically detect devices - True - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - - - - 2 - - - - - True - 0 - 6 - If you enable automatic detection of Portable Storage Devices, the entire device will be scanned for images. On large devices, this could take some time. - True - - - 1 - 2 - 2 - 3 - - - - - - - - - - - 1 - - - - - True - - - False - 2 - - - - - False - 2 - - - - - True - 12 - - - True - 2 - 2 - - - True - 0 - 12 - Location: - - - 1 - 2 - GTK_FILL - GTK_SHRINK - - - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 0 - 12 - 12 - If you disable automatic detection, choose the exact location of the images and videos. - True - - - 2 - - - - - - - - 0 - - - - - - - - True - - - False - 2 - - - - - False - 3 - - - - - False - 12 - 1 - - - - - 6 - - - - - True - Devices - - - 6 - False - tab - - - - - True - - - True - - - True - 6 - - - True - drive-removable-media - - - False - 0 - - - - - True - <span weight="bold" size="x-large">Backup</span> - True - - - False - 1 - - - - - 0 - - - - - True - - - False - 1 - - - - - False - False - 0 - - - - - True - - - 0 - 12 - <b>Backup</b> - True - - - False - 0 - - - - - True - 12 - - - True - - - False - 0 - - - - - True - 9 - 4 - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 0 - 12 - If you disable automatic detection, choose the exact backup location. - True - - - 1 - 4 - 7 - 8 - - - - - Automatically detect backup devices - True - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - - - - 1 - 4 - 2 - 3 - GTK_FILL - - - - - True - 0 - 12 - You can have your photos and videos backed up to multiple locations as they are downloaded, e.g. external hard drives. - True - - - 4 - - - - - - Backup photos and videos when downloading - True - True - False - True - True - - - - 4 - 1 - 2 - - - - - - True - 0 - 6 - Specify the folder in which backups are stored on the device. - -<i>Note: this will also be used to determine whether or not the device is used for backups. For each device you wish to use for backing up to, create a folder in it with one of these names.</i> - True - True - - - 2 - 4 - 3 - 4 - - - - - True - 0 - Backup location: - - - 1 - 3 - 8 - 9 - GTK_FILL - - - - - - True - 0 - Photo backup folder name: - - - 2 - 3 - 6 - 5 - GTK_FILL - - 6 - - - - - True - 0 - 0 - 6 - <i>Example:</i> - True - - - 2 - 3 - 6 - 7 - GTK_FILL - - - - - True - 0 - 0 - 6 - <i>/media/externaldrive/Photos</i> - True - - - 3 - 4 - 6 - 7 - - - - - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - - - - 3 - 4 - 4 - 5 - - 6 - - - - - True - 0 - Video backup folder name: - - - 2 - 3 - 5 - 6 - GTK_FILL - - 6 - - - - - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - - - - - 3 - 4 - 5 - 6 - - 6 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - True - - - False - 2 - - - - - False - 1 - - - - - 12 - 1 - - - - - 7 - - - - - True - Backup - - - 7 - False - tab - - - - - True - - - True - - - True - 6 - - - True - gtk-execute - - - False - 0 - - - - - True - <span weight="bold" size="x-large">Automation</span> - True - - - False - 1 - - - - - 0 - - - - - True - - - False - 1 - - - - - False - False - 0 - - - - - True - - - 0 - 12 - <b>Program Automation</b> - True - - - False - 0 - - - - - True - 6 - - - - False - 0 - - - - - True - 7 - 3 - - - Unmount ("eject") device upon download completion - True - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - - - - 3 - 2 - 3 - - - - - Start downloading at program startup - True - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - - - - 3 - - - - - Start downloading upon device insertion - True - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - - - - 3 - 1 - 2 - - - - - Exit program if download completes without any warnings or errors - True - True - False - True - - - - 3 - 3 - 4 - - - - - Delete photos and videos from device upon download completion - True - True - False - True - - - - 3 - 4 - 5 - - - - - - - - - - - - - - - - - - - - - - - 24 - 1 - - - - - True - - - False - 2 - - - - - False - 12 - 1 - - - - - 12 - 1 - - - - - 8 - - - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - Automation - - - 8 - False - tab - - - - - True - 12 - - - True - - - True - 6 - - - True - gtk-dialog-error - - - False - 0 - - - - - True - <span weight="bold" size="x-large">Error Handling</span> - True - - - False - 1 - - - - - 0 - - - - - True - - - False - 1 - - - - - False - False - 0 - - - - - True - 12 - - - True - - - False - 0 - - - - - True - 14 - 2 - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - - - - 2 - 3 - GTK_FILL - GTK_FILL - - - - - True - 0 - <b>Photo and Video Name Conflicts</b> - True - - - 2 - GTK_FILL - - - - - Add unique identifier - True - True - False - True - True - True - skip_download_radiobutton - - - - 1 - 2 - 3 - 4 - GTK_FILL - - - - - Skip download - True - True - False - True - True - True - - - 1 - 2 - 2 - 3 - GTK_FILL - - - - - True - 0 - 12 - When a photo or video of the same name has already been downloaded, choose whether to skip downloading the file, or to add a unique indentifier. - True - - - 1 - 2 - 1 - 2 - GTK_FILL - - - - - True - 0 - 12 - When backing up, choose whether to overwrite a file on the backup device that has the same name, or skip backing it up. - True - - - 1 - 2 - 4 - 5 - GTK_FILL - - - - - Overwrite - True - True - False - True - True - backup_duplicate_skip_radiobutton - - - - 1 - 2 - 5 - 6 - - - - - Skip - True - True - False - True - True - - - - 1 - 2 - 6 - 7 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - True - - - False - 2 - - - - - False - 12 - 1 - - - - - 9 - - - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - Error Handling - - - 9 - False - tab - - - - - 6 - 1 - - - - - 5 - 1 - - - - - - - - True - end - - - gtk-help - -11 - True - True - True - True - - - False - False - 0 - True - - - - - gtk-close - -7 - True - True - False - True - - - - False - False - 1 - - - - - False - end - 0 - - - - - - - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 5 - True - rapid-photo-downloader.svg - normal - Rapid Photo Downloader - Copyright Damon Lynch 2007-11 - Import your photos and videos efficiently and reliably - http://www.damonlynch.net/rapid - Rapid Photo Downloader is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. - -Rapid Photo Downloader is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with Rapid Photo Downloader; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - Damon Lynch <damonlynch@gmail.com> - Anton Alyab'ev <subeditor@dolgopa.org> -Lőrincz András <level.andrasnak@gmail.com> -Michel Ange <michelange@wanadoo.fr> -Alain J. Baudrez <a.baudrez@gmail.com> -Bert <crinbert@yahoo.com> -Martin Dahl Moe -Martin Egger <martin.egger@gmx.net> -Miroslav Matejaš <silverspace@ubuntu-hr.org> -Nicolás M. Zahlut <nzahlut@live.com> -Erik M -Jose Luis Navarro <jlnavarro111@gmail.com> -Tomas Novak <kuvaly@seznam.cz> -Abel O'Rian <abel.orian@gmail.com> -Balazs Oveges <ovegesb@freemail.hu> -Daniel Paessler <daniel@paessler.org> -Miloš Popović <gpopac@gmail.com> -Michal Predotka <mpredotka@googlemail.com> -Ye Qing <allen19920930@gmail.com> -Luca Reverberi <thereve@gmail.com> -Mikko Ruohola <polarfox@polarfox.net> -Sergiy Gavrylov <sergiovana@bigmir.net> -Sergei Sedov <sedov@webmail.perm.ru> -Marco Solari <marcosolari@gmail.com> -Toni Lähdekorpi <toni@lygon.net> -Ulf Urdén <ulf.urden@purplescout.com> -Julien Valroff <julien@kirya.net> -Aron Xu <happyaron.xu@gmail.com> -梁其学 <yalongbay@gmail.com> - rapid-photo-downloader.svg - True - - - True - 2 - - - - - - True - end - - - False - end - 0 - - - - - - - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - Rapid Photo Downloader - 650 - rapid-photo-downloader.svg - - - - - True - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - - - True - _File - True - - - - - Download / Pause - True - False - - - - - True - gtk-convert - - - - - - - gtk-refresh - True - True - True - - - - - - - gtk-preferences - True - True - True - - - - - - - gtk-quit - True - True - True - - - - - - - - - - True - _Select - True - - - - - True - Select _All - True - - - - - - - True - Select All Pho_tos - True - - - - - - - True - Select All Vi_deos - True - - - - - - - True - Se_lect None - True - - - - - - - True - - - - - True - Select All Without _Job Code - True - - - - - - - True - Select All Wit_h Job Code - True - - - - - - - - - - - True - _View - True - - - - - True - _Preview - True - - - - - - True - P_review Columns - True - - - True - - - True - _Type - True - - - - - - True - _Size - True - - - - - - True - _Device - True - - - - - - True - _Filename - True - - - - - - True - _Path - True - - - - - - - - - - True - Preview _Folders - True - - - - - - True - - - - - gtk-zoom-in - True - True - True - - - - - - - - gtk-zoom-out - True - True - True - - - - - - - True - - - - - True - _Error Log - True - - - - - - True - _Clear Completed Downloads - True - - - - - - True - - - - - - - - - True - _Help - True - - - - - _Get Help Online... - True - True - False - - - - - True - help - - - - - - - True - _Report a Problem... - True - - - - - - True - _Make a Donation... - True - - - - - - True - _Translate this Application... - True - - - - - - True - - - - - gtk-about - True - True - True - - - - - - - - - - False - 0 - - - - - True - 12 - - - True - True - - - True - - - True - 6 - - - True - True - automatic - automatic - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - queue - - - True - - - - - - - - - - 12 - 0 - - - - - 0 - - - - - False - False - - - - - True - 2 - 3 - - - True - - - - - - 1 - 2 - - - - - - - - True - False - - - - - 0 - - - - - True - - - - - - False - False - 1 - - - - - 12 - 1 - - - - - True - - - True - - - - - - False - 0 - - - - - True - False - - - 1 - - - - - True - - - False - 2 - - - - - True - - - True - - - False - 0 - - - - - True - - - True - - 9 - - - False - 0 - - - - - True - - - - True - - - True - - - False - 0 - - - - - True - 3 - gtk-dialog-error - 1 - - - False - False - 1 - - - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 3 - gtk-dialog-warning - 1 - - - False - False - 2 - - - - - - - False - False - 1 - - - - - 1 - - - - - False - 3 - - - - - True - - - False - 4 - - - - - 15 - True - - - False - False - 5 - - - - - False - False - end - 2 - - - - - - - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 5 - Error Log - 600 - 400 - True - rapid-photo-downloader.svg - dialog - - - - - True - 2 - - - True - True - automatic - automatic - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - - - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - False - False - - - - - - - 1 - - - - - True - end - - - - - - gtk-close - True - True - False - True - - - False - False - 1 - - - - - False - end - 0 - - - - - - diff --git a/rapid/glade3/rapid.ui b/rapid/glade3/rapid.ui new file mode 100644 index 0000000..50306f0 --- /dev/null +++ b/rapid/glade3/rapid.ui @@ -0,0 +1,990 @@ + + + + + + About... + gtk-about + + + + Check All + + + + Check All Photos + + + + Check All Videos + + + + + Download + system-run + False + + + + Get Help Online... + gtk-help + + + + Help + + + + Next File + Next file + gtk-go-forward + True + + + + Preferences + + + + Previous File + Previous file + gtk-go-back + True + + + + Quit + gtk-quit + + + + Refresh + gtk-refresh + + + + Report a Problem... + gtk-dialog-warning + + + + Show image + + + + Show thumbnails + + + + Translate this Application... + + + + Uncheck All + + + + False + Rapid Photo Downloader + + + + + True + False + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + True + + + True + False + False + _File + True + + + False + True + + + Download / Pause + True + False + download_action + False + + + + + + True + False + refresh_action + True + True + + + + + + gtk-preferences + True + False + preferences_action + True + True + + + + + + gtk-quit + True + False + quit_action + False + True + True + + + + + + + + + + True + False + False + _Select + True + + + False + True + + + True + False + check_all_action + + + + + + True + False + check_all_photos_action + + + + + + True + False + check_all_videos_action + + + + + + True + False + uncheck_all_action + + + + + + False + True + + + + + False + True + False + Select All Without _Job Code + True + + + + + + False + True + False + Select All Wit_h Job Code + True + + + + + + + + + + True + False + False + _View + True + + + False + True + + + gtk-zoom-in + False + True + False + True + True + + + + + + + gtk-zoom-out + False + True + False + True + True + + + + + + False + True + + + + + True + False + False + _Error Log + True + + + + + True + False + False + _Clear Completed Downloads + True + + + + + True + False + + + + + True + False + prev_image_action + + + + + + True + False + next_image_action + + + + + + + + + + True + False + False + _Help + True + + + False + True + + + True + False + get_help_action + True + True + + + + + + True + False + report_problem_action + + + + + True + False + donate_action + False + _Make a Donation... + True + + + + + True + False + translate_action + False + _Translate this Application... + True + + + + + True + False + + + + + gtk-about + True + False + about_action + False + True + True + + + + + + + + + False + True + 0 + + + + + True + True + 1 + True + + + True + True + automatic + never + + + True + False + queue + none + + + + + + + + False + False + + + + + True + False + + + True + True + False + False + + + True + False + + + True + True + automatic + automatic + + + + + + True + True + 0 + + + + + True + False + + + True + False + 6 + + + + + + True + True + True + show_image_action + none + False + + + False + False + 1 + + + + + _Check All + True + True + True + check_all_action + none + True + False + + + False + False + 2 + + + + + _Uncheck All + True + True + True + uncheck_all_action + none + True + False + + + False + False + 3 + + + + + False + True + 4 + 0 + + + + + + + + False + True + 1 + + + + + + + True + False + page 1 + + + False + + + + + True + False + + + True + False + True + + + + + + + True + True + 0 + + + + + True + False + True + + + True + False + 6 + + + True + True + True + next_image_action + False + none + False + + + + + + + False + True + end + 0 + + + + + True + True + True + prev_image_action + False + none + False + + + + + + + False + True + end + 1 + + + + + True + True + 6 + end + 0 + + + + + True + False + + + True + True + True + show_thumbnails_action + none + True + False + + + + + + False + False + 4 + 0 + + + + + True + True + 1 + + + + + True + False + + + _Include in download + True + True + False + False + True + True + + + + + True + True + 0 + + + + + True + False + 2 + + + + + False + True + 1 + + + + + 1 + + + + + True + False + page 2 + + + 1 + False + + + + + + + + True + False + page 3 + + + 2 + False + + + + + True + True + 0 + + + + + True + True + + + + + True + True + 1 + + + + + True + False + + + True + False + edge + + + _Help + True + True + True + help_action + True + + + False + False + 0 + + + + + _Download + True + True + True + True + True + download_action + True + + + False + False + 1 + + + + + True + True + 6 + 0 + + + + + False + True + 6 + 2 + + + + + True + False + + + True + False + + + 0 + True + False + + + False + True + 0 + + + + + False + True + 0 + + + + + True + False + 2 + False + + + True + True + 1 + + + + + True + False + + + False + True + 2 + + + + + True + False + + + True + False + + + False + True + 0 + + + + + True + False + + + True + False + + 9 + + + False + True + 0 + + + + + True + False + + + True + False + + + True + False + + + False + True + 0 + + + + + True + False + 3 + gtk-dialog-error + 1 + + + False + False + 1 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 3 + gtk-dialog-warning + 1 + + + False + False + 2 + + + + + + + False + False + 1 + + + + + True + True + 1 + + + + + False + True + 3 + + + + + True + False + + + False + True + 4 + + + + + 15 + True + False + 2 + + + False + False + 5 + + + + + False + False + end + 3 + + + + + + diff --git a/rapid/glade3/thumbnails_icon.png b/rapid/glade3/thumbnails_icon.png new file mode 100644 index 0000000..d76d954 Binary files /dev/null and b/rapid/glade3/thumbnails_icon.png differ diff --git a/rapid/glade3/video.png b/rapid/glade3/video.png deleted file mode 100644 index 2dfc666..0000000 Binary files a/rapid/glade3/video.png and /dev/null differ diff --git a/rapid/glade3/video.svg b/rapid/glade3/video.svg new file mode 100644 index 0000000..0817d62 --- /dev/null +++ b/rapid/glade3/video.svg @@ -0,0 +1,956 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rapid/glade3/video24.png b/rapid/glade3/video24.png deleted file mode 100644 index df8b22b..0000000 Binary files a/rapid/glade3/video24.png and /dev/null differ diff --git a/rapid/glade3/video66.png b/rapid/glade3/video66.png new file mode 100644 index 0000000..5502609 Binary files /dev/null and b/rapid/glade3/video66.png differ diff --git a/rapid/glade3/video_small.png b/rapid/glade3/video_small.png deleted file mode 100644 index 74e7c0f..0000000 Binary files a/rapid/glade3/video_small.png and /dev/null differ diff --git a/rapid/glade3/video_small_shadow.png b/rapid/glade3/video_small_shadow.png deleted file mode 100644 index bf39c21..0000000 Binary files a/rapid/glade3/video_small_shadow.png and /dev/null differ diff --git a/rapid/gnomeglade.py b/rapid/gnomeglade.py deleted file mode 100644 index c0b0860..0000000 --- a/rapid/gnomeglade.py +++ /dev/null @@ -1,166 +0,0 @@ -### Copyright (C) 2002-2006 Stephen Kennedy - -### This program is free software; you can redistribute it and/or modify -### it under the terms of the GNU General Public License as published by -### the Free Software Foundation; either version 2 of the License, or -### (at your option) any later version. - -### This program is distributed in the hope that it will be useful, -### but WITHOUT ANY WARRANTY; without even the implied warranty of -### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -### GNU General Public License for more details. - -### You should have received a copy of the GNU General Public License -### along with this program; if not, write to the Free Software -### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -"""Utility classes for working with glade files. - -""" -# modified by Damon Lynch May 2009 to update i18n - -import gtk -import gtk.glade -import gnome -import gnome.ui -#import gettext -import config -from common import Configi18n - -class Base(object): - """Base class for all glade objects. - - This class handles loading the xml glade file and connects - all methods name 'on_*' to the signals in the glade file. - - The handle to the xml file is stored in 'self.xml'. The - toplevel widget is stored in 'self.widget'. - - In addition it calls widget.set_data("pyobject", self) - this - allows us to get the python object given only the 'raw' gtk+ - object, which is sadly sometimes necessary. - """ - - def __init__(self, file, root, override={}): - """Load the widgets from the node 'root' in file 'file'. - - Automatically connects signal handlers named 'on_*'. - """ - global _ - _ = Configi18n._ - if Configi18n.locale_path: - gtk.glade.bindtextdomain(config.APP_NAME, Configi18n.locale_path) - gtk.glade.textdomain(config.APP_NAME) - self.xml = gtk.glade.XML(file, root, typedict=override ) - handlers = {} - for h in filter(lambda x:x.startswith("on_"), dir(self.__class__)): - handlers[h] = getattr(self, h) - self.xml.signal_autoconnect( handlers ) - self.widget = getattr(self, root) - self.widget.set_data("pyobject", self) - - def __getattr__(self, key): - """Allow glade widgets to be accessed as self.widgetname. - """ - widget = self.xml.get_widget(key) - if widget: # cache lookups - setattr(self, key, widget) - return widget - raise AttributeError(key) - - def flushevents(self): - """Handle all the events currently in the main queue and return. - """ - while gtk.events_pending(): - gtk.main_iteration(); - - def _map_widgets_into_lists(self, widgetnames): - """Put sequentially numbered widgets into lists. - - e.g. If an object had widgets self.button0, self.button1, ..., - then after a call to object._map_widgets_into_lists(["button"]) - object has an attribute self.button == [self.button0, self.button1, ...]." - """ - for item in widgetnames: - setattr(self,item, []) - lst = getattr(self,item) - i = 0 - while 1: - key = "%s%i"%(item,i) - try: - val = getattr(self, key) - except AttributeError: - break - lst.append(val) - i += 1 - - -class Component(Base): - """A convenience base class for widgets which use glade. - """ - - def __init__(self, file, root, override={}): - Base.__init__(self, file, root, override) - - -class GtkApp(Base): - """A convenience base class for gtk+ apps created in glade. - """ - - def __init__(self, file, root=None): - Base.__init__(self, file, root) - - def main(self): - """Enter the gtk main loop. - """ - gtk.main() - - def quit(self, *args): - """Signal the gtk main loop to quit. - """ - gtk.main_quit() - - -class GnomeApp(GtkApp): - """A convenience base class for apps created in glade. - """ - - def __init__(self, name, version, file, root): - """Initialise program 'name' and version from 'file' containing root node 'root'. - """ - self.program = gnome.program_init(name, version) - GtkApp.__init__(self,file,root) - if 0: - self.client = gnome.ui.Client() - self.client.disconnect() - def connected(*args): - print "CONNECTED", args - def cb(name): - def cb2(*args): - print name, args, "\n" - return cb2 - self.client.connect("connect", cb("CON")) - self.client.connect("die", cb("DIE")) - self.client.connect("disconnect", cb("DIS")) - self.client.connect("save-yourself", cb("SAVE")) - self.client.connect("shutdown-cancelled", cb("CAN")) - self.client.connect_to_session_manager() - - -def load_pixbuf(fname, size=0): - """Load an image from a file as a pixbuf, with optional resizing. - """ - image = gtk.Image() - image.set_from_file(fname) - image = image.get_pixbuf() - if size: - aspect = float(image.get_height()) / image.get_width() - image = image.scale_simple(size, int(aspect*size), 2) - return image - -def url_show(url): - return gnome.url_show(url) - -def FileEntry(*args): - return gnome.ui.FileEntry(*args) - diff --git a/rapid/idletube.py b/rapid/idletube.py deleted file mode 100644 index 0b07536..0000000 --- a/rapid/idletube.py +++ /dev/null @@ -1,205 +0,0 @@ - -# Copyright (c) 2005 Antoon Pardon -# -# Modified 2010 by Damon Lynch to use python's higher performance deque, rather than a regular list -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -import collections -from Queue import Queue - -from threading import Lock -from thread import get_ident - -from types import BooleanType as UnConnected - -UnRegistered, Registered = False, True - - -class EOInformation(Exception): - pass - -class TubeAccess(Exception): - pass - - -class Fifo: - - def __init__(self): - self.fifo = collections.deque() - - def put(self, item): - self.fifo.append(item) - - def get(self): - return self.fifo.popleft() - - def size(self): - return len(self.fifo) - - -class Tube: - - def __init__(self, maxsize, lck = Lock, container = None): - if container is None: - container = Fifo() - self.readers = set() - self.writers = set() - self.container = container - self.maxsize = maxsize - self.cb_arglst = [] - self.cb_src = UnRegistered - self.in_use = Lock() - self.nowriter = lck() - self.full = lck() - self.empty = lck() - self.empty.acquire() - self.nowriter.acquire() - - def open(self, access = 'r', *to): - thrd = get_ident() - access = access.lower() - self.in_use.acquire() - if 'w' in access: - if len(self.writers) == 0: - for _ in self.readers: - self.nowriter.release() - self.writers.add(thrd) - if 'r' in access: - self.readers.add(thrd) - if len(self.writers) == 0: - self.in_use.release() - self.nowriter.acquire(*to) - else: - self.in_use.release() - else: - self.in_use.release() - - def close(self, access = 'rw'): - thrd = get_ident() - access = access.lower() - self.in_use.acquire() - if 'r' in access: - self.readers.discard(thrd) - if 'w' in access: - self.writers.discard(thrd) - if len(self.writers) == 0: - if self.container.size() == 0: - self.empty.release() - if self.cb_src is Registered and len(self.readers) > 0: - self.cb_src = gob.idle_add(self._idle_callback) - for _ in self.readers: - self.container.put(EOInformation) - self.in_use.release() - - def size(self): - self.in_use.acquire() - size = self.container.size() - self.in_use.release() - return size - - def get(self, *to): - thrd = get_ident() - if thrd not in self.readers: - raise TubeAccess, "Thread has no read access for tube" - self.empty.acquire(*to) - self.in_use.acquire() - size = self.container.size() - if size == self.maxsize: - self.full.release() - item = self.container.get() - if size != 1: - self.empty.release() - elif type(self.cb_src) is not UnConnected: - gob.source_remove(self.cb_src) - self.cb_src = Registered - self.in_use.release() - if item is EOInformation: - raise EOInformation - else: - return item - - def put(self, item, *to): - thrd = get_ident() - if thrd not in self.writers: - raise TubeAccess, "Thread has no write access for tube" - if thrd in self.readers: - self._put_rw(item) - else: - self._put_wo(item, *to) - - def _put_wo(self, item, *to): - self.full.acquire(*to) - self.in_use.acquire() - size = self.container.size() - if size == 0: - self.empty.release() - if self.cb_src is Registered: - self.cb_src = gob.idle_add(self._idle_callback) - self.container.put(item) - if size + 1 < self.maxsize: - self.full.release() - self.in_use.release() - - def _put_rw(self, item): - self.in_use.acquire() - size = self.container.size() - if size == 0: - self.empty.release() - if self.cb_src is Registered: - self.cb_src = gob.idle_add(self._idle_callback) - self.container.put(item) - self.in_use.release() - - def _idle_callback(self): - self.in_use.acquire() - lst = self.cb_arglst.pop(0) - self.in_use.release() - func = lst[0] - lst[0] = self - ret_val = func(*lst) - self.in_use.acquire() - if ret_val: - lst[0] = func - self.cb_arglst.append(lst) - elif self.cb_arglst == []: - self.cb_src = UnRegistered - self.in_use.release() - return self.cb_src is not UnRegistered - - -def tube_add_watch(tube, callback, *args): - - global gob - import gobject as gob - - tube.in_use.acquire() - tube.cb_arglst.append([callback] + list(args)) - if tube.cb_src is UnRegistered: - if tube.container.size() == 0: - tube.cb_src = Registered - else: - tube.cb_src = gob.idle_add(tube._idle_callback) - tube.in_use.release() - -def tube_remove_watch(tube): -## tube.in_use.acquire() -## gob.source_remove(tube.cb_src) -## tube._idle_callback.handler_block(tube.cb_src) - pass diff --git a/rapid/media.py b/rapid/media.py deleted file mode 100755 index 9819ab7..0000000 --- a/rapid/media.py +++ /dev/null @@ -1,339 +0,0 @@ -#!/usr/bin/python -# -*- coding: latin1 -*- - -### Copyright (C) 2007, 2008, 2009, 2010 Damon Lynch - -### This program is free software; you can redistribute it and/or modify -### it under the terms of the GNU General Public License as published by -### the Free Software Foundation; either version 2 of the License, or -### (at your option) any later version. - -### This program is distributed in the hope that it will be useful, -### but WITHOUT ANY WARRANTY; without even the implied warranty of -### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -### GNU General Public License for more details. - -### You should have received a copy of the GNU General Public License -### along with this program; if not, write to the Free Software -### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -import os -import sys -import types -import datetime -import subprocess - -import config -from config import max_thumbnail_size -from config import STATUS_NOT_DOWNLOADED, \ - STATUS_DOWNLOAD_PENDING, \ - STATUS_CANNOT_DOWNLOAD - - -import common -import metadata -import videometadata - -from common import Configi18n -global _ -_ = Configi18n._ - -import operator -import gtk - -def _getDefaultLocationLegacy(options, ignore_missing_dir=False): - if ignore_missing_dir: - return common.getFullPath(options[0]) - for default in options: - path = common.getFullPath(default) - if os.path.isdir(path): - return path - return common.getFullPath('') - -def _getDefaultLocationXDG(dir_type): - proc = subprocess.Popen(['xdg-user-dir', dir_type], stdout=subprocess.PIPE) - output = proc.communicate()[0].strip() - return output - -def getDefaultPhotoLocation(ignore_missing_dir=False): - try: - return _getDefaultLocationXDG('PICTURES') - except: - return _getDefaultLocationLegacy(config.DEFAULT_PHOTO_LOCATIONS, ignore_missing_dir) - -def getDefaultVideoLocation(ignore_missing_dir=False): - try: - return _getDefaultLocationXDG('VIDEOS') - except: - return _getDefaultLocationLegacy(config.DEFAULT_VIDEO_LOCATIONS, ignore_missing_dir) - -def getDefaultBackupPhotoIdentifier(): - return os.path.split(getDefaultPhotoLocation(ignore_missing_dir = True))[1] - -def getDefaultBackupVideoIdentifier(): - return os.path.split(getDefaultVideoLocation(ignore_missing_dir = True))[1] - -def is_DCIM_Media(path): - """ Returns true if directory specifies some media with photos on it """ - - if os.path.isdir(os.path.join(path, "DCIM")): - # is very likely a memory card, or something like that! - return True - else: - return False - - -def isBackupMedia(path, identifiers, writeable=True): - """ Test to see if path is used as a backup medium for storing photos or videos - - Identifiers is expected to be a list of folder names to check to see - if the path is a backup path. Only one of them needs to be present - for the path to be considered a backup medium. - - If writeable is True, the directory must be writeable by the user """ - suitable = False - - for identifier in identifiers: - if os.path.isdir(os.path.join(path, identifier)): - if writeable: - suitable = os.access(os.path.join(path, identifier), os.W_OK) - else: - suitable = True - if suitable: - return True - return False - -def isImage(fileName): - ext = os.path.splitext(fileName)[1].lower()[1:] - return (ext in metadata.RAW_FILE_EXTENSIONS) or (ext in metadata.NON_RAW_IMAGE_FILE_EXTENSIONS) - -def isVideo(fileName): - ext = os.path.splitext(fileName)[1].lower()[1:] - return (ext in videometadata.VIDEO_FILE_EXTENSIONS) - - -class MediaFile: - """ - A photo or video file, with metadata - """ - - def __init__(self, thread_id, name, path, size, fileSystemModificationTime, deviceName, downloadFolder, volume, isPhoto = True): - self.thread_id = thread_id - self.path = path - self.name = name - self.fullFileName = os.path.join(path, name) - self.size = size # type int - self.modificationTime = fileSystemModificationTime - self.deviceName = deviceName - self.downloadFolder = downloadFolder - self.volume = volume - - self.jobcode = '' - - # a reference into the SelectionTreeView's liststore - self.treerowref = None - - # generated values - self.downloadSubfolder = '' - self.downloadPath = '' - self.downloadName = '' - self.downloadFullFileName = '' - - self.isImage = isPhoto - self.isVideo = not self.isImage - if isPhoto: - self.displayName = _("photo") - self.displayNameCap = _("Photo") - else: - self.displayName = _("video") - self.displayNameCap = _("Video") - - - self.metadata = None - self.thumbnail = None - self.genericThumbnail = False - self.sampleName = '' - self.sampleSubfolder = '' - self.samplePath = '' - - # whether the sample genereated name, subfolder and path need to be refreshed in a preview - self.sampleStale = False - - self.status = STATUS_NOT_DOWNLOADED - self.problem = None # class Problem in problemnotifcation.py - - def loadMetadata(self): - """ - Attempt to load the metadata for the photo or video - - Raises errors if unable to be loaded - """ - if not self.metadata: - if self.isImage: - self.metadata = metadata.MetaData(self.fullFileName) - self.metadata.read() - else: - self.metadata = videometadata.VideoMetaData(self.fullFileName) - - - def dateTime(self, alternative_if_date_missing=None): - date = None - if self.metadata: - date = self.metadata.dateTime() - if not date: - if alternative_if_date_missing: - date = alternative_if_date_missing - else: - date = datetime.datetime.fromtimestamp(self.modificationTime) - return date - - - def generateThumbnail(self, tempWorkingDir): - """ - Attempts to generate or extract a thumnail and its orientation for the photo or video - """ - if self.metadata is None: - sys.stderr.write("metadata should not be empty!") - else: - if self.isImage: - try: - thumbnail = self.metadata.getThumbnailData(max_thumbnail_size) - if not isinstance(thumbnail, types.StringType): - self.thumbnail = None - else: - orientation = self.metadata.orientation(missing=None) - pbloader = gtk.gdk.PixbufLoader() - pbloader.write(thumbnail) - pbloader.close() - # Get the resulting pixbuf and build an image to be displayed - pixbuf = pbloader.get_pixbuf() - if orientation == 8: - pixbuf = pixbuf.rotate_simple(gtk.gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE) - elif orientation == 6: - pixbuf = pixbuf.rotate_simple(gtk.gdk.PIXBUF_ROTATE_CLOCKWISE) - elif orientation == 3: - pixbuf = pixbuf.rotate_simple(gtk.gdk.PIXBUF_ROTATE_UPSIDEDOWN) - - self.thumbnail = pixbuf - except: - pass - else: - # get thumbnail of video - # it may need to be generated - self.thumbnail = self.metadata.getThumbnailData(max_thumbnail_size, tempWorkingDir) - if self.thumbnail: - # scale to size - self.thumbnail = common.scale2pixbuf(max_thumbnail_size, max_thumbnail_size, self.thumbnail) - - - -class Media: - """ Generic class for media holding images and videos """ - def __init__(self, path, volume = None): - """ - volume is a gnomevfs or gio volume: see class Volume in rapid.py - """ - - self.path = path - self.volume = volume - - - def prettyName(self, limit=config.MAX_LENGTH_DEVICE_NAME): - """ - Returns a name for the media, useful for display. - - If the media is from a gnomevfs volume, returns the gnome name. - - Else. returns the last part of the mount point after stripping out - underscores. - """ - - if self.volume: - return self.volume.get_name(limit) - else: - name = os.path.split(self.path)[1] - name = name.replace('_', ' ') - v = name - if limit: - if len(v) > limit: - v = v[:limit] + '...' - return v - - def getPath(self): - return self.path - - -class CardMedia(Media): - """Compact Flash cards, hard drives, etc.""" - def __init__(self, path, volume = None): - """ - volume is a gnomevfs or gio volume, see class Volume in rapid.py - """ - Media.__init__(self, path, volume) - - - def setMedia(self, imagesAndVideos, fileSizeSum, noFiles): - self.imagesAndVideos = imagesAndVideos # class MediaFile - self.fileSizeSum = fileSizeSum - self.noFiles = noFiles - - def numberOfImagesAndVideos(self): - return self.noFiles - - def sizeOfImagesAndVideos(self, humanReadable = True): - if humanReadable: - return common.formatSizeForUser(self.fileSizeSum) - else: - return self.fileSizeSum - - def sizeAndNumberDownloadPending(self): - """ - Returns how many files have their status set to download pending, and their size - """ - v = s = 0 - fileIndex = [] - for i in range(len(self.imagesAndVideos)): - mediaFile = self.imagesAndVideos[i][0] - if mediaFile.status == STATUS_DOWNLOAD_PENDING: - v += 1 - s += mediaFile.size - fileIndex.append(i) - return (v, s, fileIndex) - - def numberOfFilesNotCannotDownload(self): - """ - Returns how many files whose status is not cannot download - """ - v = 0 - for i in range(len(self.imagesAndVideos)): - mediaFile = self.imagesAndVideos[i][0] - if mediaFile.status <> STATUS_CANNOT_DOWNLOAD: - v += 1 - - return v - - def downloadPending(self): - """ - Returns true if there a mediaFile with status download pending on the device. - Inefficient. Not currently used. - """ - for i in range(len(self.imagesAndVideos)): - mediaFile = self.imagesAndVideos[i][0] - if mediaFile.status == config.STATUS_DOWNLOAD_PENDING: - return True - return False - - def _firstFile(self, isImage): - if self.imagesAndVideos: - for i in range(len(self.imagesAndVideos)): - if self.imagesAndVideos[i][0].isImage == isImage: - return self.imagesAndVideos[i][0] - else: - return None - - def firstImage(self): - return self._firstFile(True) - - def firstVideo(self): - return self._firstFile(False) - diff --git a/rapid/metadata.py b/rapid/metadata.py deleted file mode 100755 index 1a040f3..0000000 --- a/rapid/metadata.py +++ /dev/null @@ -1,587 +0,0 @@ -#!/usr/bin/python -# -*- coding: latin1 -*- - -### Copyright (C) 2007-10 Damon Lynch - -### This program is free software; you can redistribute it and/or modify -### it under the terms of the GNU General Public License as published by -### the Free Software Foundation; either version 2 of the License, or -### (at your option) any later version. - -### This program is distributed in the hope that it will be useful, -### but WITHOUT ANY WARRANTY; without even the implied warranty of -### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -### GNU General Public License for more details. - -### You should have received a copy of the GNU General Public License -### along with this program; if not, write to the Free Software -### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -import re -import datetime -import sys -import subprocess -import config -import types -import time - -try: - import pyexiv2 -except ImportError: - sys.stderr.write("You need to install pyexiv2, the python binding for exiv2, to run this program.\n" ) - sys.exit(1) - -#only pyexiv2 <= 0.1.1 does not use the "Rational" class -if 'Rational' in dir(pyexiv2): - usesRational = True -else: - usesRational = False - -#get versions of pyexiv2 and exiv2 libraries -if 'version_info' in dir(pyexiv2): - pyexiv2_version = pyexiv2.version_info - exiv2_version = pyexiv2.exiv2_version_info - baseclass = eval('pyexiv2.metadata.ImageMetadata') -else: - pyexiv2_version = (0,1,'x') - # try to determine the version of exiv2 from it's standard output - try: - proc = subprocess.Popen(['exiv2', '-V'], stdout=subprocess.PIPE) - output = proc.communicate()[0] - except: - output = None - exiv2_version = None - if output: - # assume output contains the line 'exiv2 0.x' or possibly - # 'exiv2 0.x.x' - start = output.find('exiv2 ') - if start < 0: - exiv2_version = None - else: - end = output.find('\n', start) - if end: - exiv2_v = output[6:end] - else: - exiv2_v = output[6:] - - exiv2_version = [] - dot = exiv2_v.find('.') - while dot > 0: - exiv2_version += [int(exiv2_v[:dot])] - exiv2_v = exiv2_v[dot+1:] - dot = exiv2_v.find('.') - exiv2_version += [int(exiv2_v)] - exiv2_version = tuple(exiv2_version) - - - baseclass = eval('pyexiv2.Image') - -def __version_info(version): - if not version: - return '' - else: - v = '' - for i in version: - v += '.%s' % i - return v[1:] - -def version_info(): - return __version_info(pyexiv2_version) - -def exiv2_version_info(): - return __version_info(exiv2_version) - -RAW_FILE_EXTENSIONS = ['arw', 'dcr', 'cr2', 'crw', 'dng', 'mos', 'mrw', - 'nef', 'orf', 'pef', 'raf', 'raw', 'sr2', 'srw'] - -#exiv2 0.18.1 introduces support for Panasonic .RW2 files -#pyexiv2 in combination with exiv2 0.18 segfaults when trying to read an -#RW2 files, so we should not read those! exiv2 0.17 & pyexiv2 segfaults -#with MEF files. - -if exiv2_version is not None: - if exiv2_version[0] > 0: - RAW_FILE_EXTENSIONS += ['rw2', 'mef'] - else: - if exiv2_version[1] > 17: - RAW_FILE_EXTENSIONS += ['mef'] - if exiv2_version[1] > 18: - RAW_FILE_EXTENSIONS += ['rw2'] - else: - if len(exiv2_version) > 2: - if exiv2_version[2] >= 1: - RAW_FILE_EXTENSIONS += ['rw2'] - -RAW_FILE_EXTENSIONS.sort() - -NON_RAW_IMAGE_FILE_EXTENSIONS = ['jpg', 'jpe', 'jpeg', 'tif', 'tiff'] - - -class MetaData(baseclass): - """ - Class providing human readable access to image metadata - - """ - - __version01__ = pyexiv2_version[0] == 0 and pyexiv2_version[1] == 1 - - def aperture(self, missing=''): - """ - Returns in string format the floating point value of the image's aperture. - - Returns missing if the metadata value is not present. - """ - - try: - if usesRational: - a = self["Exif.Photo.FNumber"] - a0, a1 = str(a).split('/') - else: - a0, a1 = self["Exif.Photo.FNumber"] - a = float(a0) / float(a1) - return "%.1f" % a - except: - return missing - - def iso(self, missing=''): - """ - Returns in string format the integer value of the image's ISO. - - Returns missing if the metadata value is not present. - """ - try: - return "%s" % (self["Exif.Photo.ISOSpeedRatings"]) - except: - return missing - - def exposureTime(self, alternativeFormat=False, missing=''): - """ - Returns in string format the exposure time of the image. - - Returns missing if the metadata value is not present. - - alternativeFormat is useful if the value is going to be used in a - purpose where / is an invalid character, e.g. file system names. - - alternativeFormat is False: - For exposures less than one second, the result is formatted as a - fraction e.g. 1/125 - For exposures greater than or equal to one second, the value is - formatted as an integer e.g. 30 - - alternativeFormat is True: - For exposures less than one second, the result is formatted as an - integer e.g. 125 - For exposures less than one second but more than or equal to - one tenth of a second, the result is formatted as an integer - e.g. 3 representing 3/10 of a second - For exposures greater than or equal to one second, the value is - formatted as an integer with a trailing s e.g. 30s - """ - - try: - if usesRational: - - e = str(self["Exif.Photo.ExposureTime"]) - - e0, e1 = e.split('/') - e0 = int(e0) - e1 = int(e1) - # some values, e.g. Nikon, are in the format "10/1600" - if (e0 > 1) and (e0 < e1): - e1 = e1 / e0 - e0 = 1 - else: - e0, e1 = self["Exif.Photo.ExposureTime"] - - if e1 > e0: - if alternativeFormat: - if e0 == 1: - return str(e1) - else: - return str(e0) - else: - return "%s/%s" % (e0,e1) - elif e0 > e1: - e = float(e0) / e1 - if alternativeFormat: - return "%.0fs" % e - else: - return "%.0f" % e - else: - return "1s" - except: - return missing - - def focalLength(self, missing=''): - """ - Returns in string format the focal length of the lens used to record the image. - - Returns missing if the metadata value is not present. - """ - try: - if usesRational: - f = str(self["Exif.Photo.FocalLength"]) - f0, f1 = f.split('/') - else: - f0, f1 = self["Exif.Photo.FocalLength"] - - f0 = float(f0) - if not f1: - f1 = 1.0 - else: - f1 = float(f1) - - return "%.0f" % (f0 / f1) - except: - return missing - - - def cameraMake(self, missing=''): - """ - Returns in string format the camera make (manufacturer) used to record the image. - - Returns missing if the metadata value is not present. - """ - try: - return self["Exif.Image.Make"].strip() - except: - return missing - - def cameraModel(self, missing=''): - """ - Returns in string format the camera model used to record the image. - - Returns missing if the metadata value is not present. - """ - try: - return self["Exif.Image.Model"].strip() - except: - return missing - - def cameraSerial(self, missing=''): - try: - keys = self.rpd_keys() - if 'Exif.Canon.SerialNumber' in keys: - v = self['Exif.Canon.SerialNumber'] - elif 'Exif.Nikon3.SerialNumber' in keys: - v = self['Exif.Nikon3.SerialNumber'] - elif 'Exif.OlympusEq.SerialNumber' in keys: - v = self['Exif.OlympusEq.SerialNumber'] - elif 'Exif.Olympus.SerialNumber' in keys: - v = self['Exif.Olympus.SerialNumber'] - elif 'Exif.Olympus.SerialNumber2' in keys: - v = self['Exif.Olympus.SerialNumber2'] - elif 'Exif.Panasonic.SerialNumber' in keys: - v = self['Exif.Panasonic.SerialNumber'] - elif 'Exif.Fujifilm.SerialNumber' in keys: - v = self['Exif.Fujifilm.SerialNumber'] - elif 'Exif.Image.CameraSerialNumber' in keys: - v = self['Exif.Image.CameraSerialNumber'] - else: - return missing - v = str(v) - return v.strip() - except: - return missing - - def shutterCount(self, missing=''): - try: - keys = self.rpd_keys() - if 'Exif.Nikon3.ShutterCount' in keys: - v = self['Exif.Nikon3.ShutterCount'] - elif 'Exif.Canon.FileNumber' in keys: - v = self['Exif.Canon.FileNumber'] - elif 'Exif.Canon.ImageNumber' in keys: - v = self['Exif.Canon.ImageNumber'] - else: - return missing - return str(v) - except: - return missing - - def ownerName(self, missing=''): - """ returns camera name recorded by select Canon cameras""" - try: - return self['Exif.Canon.OwnerName'].strip() - except: - return missing - - def shortCameraModel(self, includeCharacters = '', missing=''): - """ - Returns in shorterned string format the camera model used to record the image. - - Returns missing if the metadata value is not present. - - The short format is determined by the first occurrence of a digit in the - camera model, including all alphaNumeric characters before and after - that digit up till a non-alphanumeric character, but with these interventions: - - 1. Canon "Mark" designations are shortened prior to conversion. - 2. Names like "Canon EOS DIGITAL REBEL XSi" do not have a number and must - and treated differently (see below) - - Examples: - Canon EOS 300D DIGITAL -> 300D - Canon EOS 5D -> 5D - Canon EOS 5D Mark II -> 5DMkII - NIKON D2X -> D2X - NIKON D70 -> D70 - X100,D540Z,C310Z -> X100 - Canon EOS DIGITAL REBEL XSi -> XSi - Canon EOS Digital Rebel XS -> XS - Canon EOS Digital Rebel XTi -> XTi - Canon EOS Kiss Digital X -> Digital - Canon EOS Digital Rebel XT -> XT - EOS Kiss Digital -> Digital - Canon Digital IXUS Wireless -> Wireless - Canon Digital IXUS i zoom -> zoom - Canon EOS Kiss Digital N -> N - Canon Digital IXUS IIs -> IIs - IXY Digital L -> L - Digital IXUS i -> i - IXY Digital -> Digital - Digital IXUS -> IXUS - - The optional includeCharacters allows additional characters to appear - before and after the digits. - Note: special includeCharacters MUST be escaped as per syntax of a - regular expressions (see documentation for module re) - - Examples: - - includeCharacters = '': - DSC-P92 -> P92 - includeCharacters = '\-': - DSC-P92 -> DSC-P92 - - If a digit is not found in the camera model, the last word is returned. - - Note: assume exif values are in ENGLISH, regardless of current platform - """ - m = self.cameraModel() - m = m.replace(' Mark ', 'Mk') - if m: - s = r"(?:[^a-zA-Z0-9%s]?)(?P[a-zA-Z0-9%s]*\d+[a-zA-Z0-9%s]*)"\ - % (includeCharacters, includeCharacters, includeCharacters) - r = re.search(s, m) - if r: - return r.group("model") - else: - head, space, model = m.strip().rpartition(' ') - return model - else: - return missing - - def filterMangledDates(self, d): - """ - Some EXIF dates are badly formed. Try to fix them - """ - - _datetime = d.strip() - # remove any weird characters at the end of the string - while _datetime and not _datetime[-1].isdigit(): - _datetime = _datetime[:-1] - _date, _time = _datetime.split(' ') - _datetime = "%s %s" % (_date.replace(":", "-") , _time.replace("-", ":")) - try: - d = datetime.datetime.strptime(_datetime, '%Y-%m-%d %H:%M:%S') - except: - d = None - return d - - def dateTime(self, missing=''): - """ - Returns in python datetime format the date and time the image was - recorded. - - Trys to get value from exif key "Exif.Photo.DateTimeOriginal". - If that does not exist, trys key "Exif.Image.DateTime" - - Returns missing either metadata value is not present. - """ - keys = self.rpd_keys() - try: - if "Exif.Photo.DateTimeOriginal" in keys: - v = self["Exif.Photo.DateTimeOriginal"] - else: - v = self["Exif.Image.DateTime"] - if isinstance(v, types.StringType): - v = self.filterMangledDates(v) - if v is None: - v = missing - return v - except: - return missing - - def timeStamp(self, missing=''): - dt = self.dateTime(missing=None) - if not dt is None: - try: - t = dt.timetuple() - ts = time.mktime(t) - except: - ts = missing - else: - ts = missing - return ts - - def subSeconds(self, missing='00'): - """ returns the subsecond the image was taken, as recorded by the camera""" - try: - return str(self["Exif.Photo.SubSecTimeOriginal"]) - except: - return missing - - def orientation(self, missing=''): - """ - Returns the orientation of the image, as recorded by the camera - Return type int - """ - try: - v = self['Exif.Image.Orientation'] - if isinstance(v, types.StringType): - # pyexiv2 >= 0.2 returns a string, not an int - v = int(v) - return v - except: - return missing - - # following class methods are designed to cope with using both - # pyexiv2 0.1.x and pyexiv2 0.2.x - - def getThumbnailData(self, max_size_needed=0): - """ - Returns a thumbnail of the image. - - If the image supports multiple thumbnails, and max_size_needed - is not 0, then it will search for the smallest thumbnail that - matches the size required - - The image will be in whatever format the thumbnail itself is, - typically a jpeg or tiff. - """ - if self.__version01__: - return pyexiv2.Image.getThumbnailData(self)[1] - - else: - if not self.previews: - return None, None - else: - if max_size_needed: - for thumbnail in self.previews: - if thumbnail.dimensions[0] >= max_size_needed or thumbnail.dimensions[1] >= max_size_needed: - break - else: - thumbnail = self.previews[-1] - - return thumbnail.data - - def read(self): - if self.__version01__: - self.readMetadata() - else: - pyexiv2.metadata.ImageMetadata.read(self) - - def rpd_keys(self): - if self.__version01__: - return pyexiv2.Image.exifKeys(self) - else: - return self.exif_keys - - def __getitem__(self, key): - if self.__version01__: - v = pyexiv2.Image.__getitem__(self, key) - else: - v = pyexiv2.metadata.ImageMetadata.__getitem__(self, key).raw_value - # strip out null bytes from strings - if isinstance(v, types.StringType): - v = v.replace('\x00', '') - return v - - -class DummyMetaData(MetaData): - """ - Class which gives metadata values for an imaginary image. - - Useful for displaying in preference examples etc. when no image is ready to - be downloaded. - - See MetaData class for documentation of class methods. - """ - - def __init__(self): - pass - - def readMetadata(self): - pass - - def aperture(self, missing=''): - return "2.0" - - def iso(self, missing=''): - return "100" - - def exposureTime(self, alternativeFormat=False, missing=''): - if alternativeFormat: - return "4000" - else: - return "1/4000" - - def focalLength(self, missing=''): - return "135" - - def cameraMake(self, missing=''): - return "Canon" - - def cameraModel(self, missing=''): - return "Canon EOS 5D" - - def shortCameraModel(self, includeCharacters = '', missing=''): - return "5D" - - def cameraSerial(self, missing=''): - return '730402168' - - def shutterCount(self, missing=''): - return '387' - - def ownerName(self, missing=''): - return 'Photographer Name' - - def dateTime(self, missing=''): - return datetime.datetime.now() - - def subSeconds(self, missing='00'): - return '57' - - def orientation(self, missing=''): - return 1 - -if __name__ == '__main__': - import sys - - - if (len(sys.argv) != 2): - print 'Usage: ' + sys.argv[0] + ' path/to/photo/containing/metadata' - m = DummyMetaData() - - else: - m = MetaData(sys.argv[1]) - m.read() - - print "f"+ m.aperture('missing ') - print "ISO " + m.iso('missing ') - print m.exposureTime(missing='missing ') + " sec" - print m.exposureTime(alternativeFormat=True, missing='missing ') - print m.focalLength('missing ') + "mm" - print m.cameraMake() - print m.cameraModel() - print m.shortCameraModel() - print m.shortCameraModel(includeCharacters = "\-") - print m.dateTime() - print m.orientation() - print 'Serial number:', m.cameraSerial() - print 'Shutter count:', m.shutterCount() - print 'Subseconds:', m.subSeconds() - diff --git a/rapid/metadataphoto.py b/rapid/metadataphoto.py new file mode 100755 index 0000000..8a760c4 --- /dev/null +++ b/rapid/metadataphoto.py @@ -0,0 +1,426 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007-10 Damon Lynch + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import re +import datetime +import sys +#~ import subprocess +import config +import types +import time + +try: + import pyexiv2 +except ImportError: + sys.stderr.write("You need to install pyexiv2, the python binding for exiv2, to run this program.\n" ) + sys.exit(1) + + + +def __version_info(version): + if not version: + return '' + else: + v = '' + for i in version: + v += '.%s' % i + return v[1:] + +def pyexiv2_version_info(): + return __version_info(pyexiv2.version_info) + +def exiv2_version_info(): + return __version_info(pyexiv2.exiv2_version_info) + + +class MetaData(pyexiv2.metadata.ImageMetadata): + """ + Class providing human readable access to image metadata + + """ + + def aperture(self, missing=''): + """ + Returns in string format the floating point value of the image's aperture. + + Returns missing if the metadata value is not present. + """ + + try: + + a = self["Exif.Photo.FNumber"].value + + a = float(a.numerator) / float(a.denominator) + return "%.1f" % a + except: + return missing + + def iso(self, missing=''): + """ + Returns in string format the integer value of the image's ISO. + + Returns missing if the metadata value is not present. + """ + try: + return self["Exif.Photo.ISOSpeedRatings"].human_value + except: + return missing + + def exposure_time(self, alternativeFormat=False, missing=''): + """ + Returns in string format the exposure time of the image. + + Returns missing if the metadata value is not present. + + alternativeFormat is useful if the value is going to be used in a + purpose where / is an invalid character, e.g. file system names. + + alternativeFormat is False: + For exposures less than one second, the result is formatted as a + fraction e.g. 1/125 + For exposures greater than or equal to one second, the value is + formatted as an integer e.g. 30 + + alternativeFormat is True: + For exposures less than one second, the result is formatted as an + integer e.g. 125 + For exposures less than one second but more than or equal to + one tenth of a second, the result is formatted as an integer + e.g. 3 representing 3/10 of a second + For exposures greater than or equal to one second, the value is + formatted as an integer with a trailing s e.g. 30s + """ + + try: + + e = self["Exif.Photo.ExposureTime"].value + + e0 = int(e.numerator) + e1 = int(e.denominator) + + if e1 > e0: + if alternativeFormat: + if e0 == 1: + return str(e1) + else: + return str(e0) + else: + return "%s/%s" % (e0,e1) + elif e0 > e1: + e = float(e0) / e1 + if alternativeFormat: + return "%.0fs" % e + else: + return "%.0f" % e + else: + return "1s" + except: + return missing + + def focal_length(self, missing=''): + """ + Returns in string format the focal length of the lens used to record the image. + + Returns missing if the metadata value is not present. + """ + try: + f = self["Exif.Photo.FocalLength"].value + f0 = float(f.numerator) + f1 = float(f.denominator) + + return "%.0f" % (f0 / f1) + except: + return missing + + + def camera_make(self, missing=''): + """ + Returns in string format the camera make (manufacturer) used to record the image. + + Returns missing if the metadata value is not present. + """ + try: + return self["Exif.Image.Make"].value.strip() + except: + return missing + + def camera_model(self, missing=''): + """ + Returns in string format the camera model used to record the image. + + Returns missing if the metadata value is not present. + """ + try: + return self["Exif.Image.Model"].value.strip() + except: + return missing + + def camera_serial(self, missing=''): + try: + keys = self.exif_keys + if 'Exif.Canon.SerialNumber' in keys: + v = self['Exif.Canon.SerialNumber'].raw_value + elif 'Exif.Nikon3.SerialNumber' in keys: + v = self['Exif.Nikon3.SerialNumber'].raw_value + elif 'Exif.OlympusEq.SerialNumber' in keys: + v = self['Exif.OlympusEq.SerialNumber'].raw_value + elif 'Exif.Olympus.SerialNumber' in keys: + v = self['Exif.Olympus.SerialNumber'].raw_value + elif 'Exif.Olympus.SerialNumber2' in keys: + v = self['Exif.Olympus.SerialNumber2'].raw_value + elif 'Exif.Panasonic.SerialNumber' in keys: + v = self['Exif.Panasonic.SerialNumber'].raw_value + elif 'Exif.Fujifilm.SerialNumber' in keys: + v = self['Exif.Fujifilm.SerialNumber'].raw_value + elif 'Exif.Image.CameraSerialNumber' in keys: + v = self['Exif.Image.CameraSerialNumber'].raw_value + else: + return missing + v = str(v) # probably not necessary, but just in case + return v.strip() + except: + return missing + + def shutter_count(self, missing=''): + try: + keys = self.exif_keys + if 'Exif.Nikon3.ShutterCount' in keys: + v = self['Exif.Nikon3.ShutterCount'].raw_value + elif 'Exif.Canon.FileNumber' in keys: + v = self['Exif.Canon.FileNumber'].raw_value + elif 'Exif.Canon.ImageNumber' in keys: + v = self['Exif.Canon.ImageNumber'].raw_value + else: + return missing + return str(v) + except: + return missing + + def owner_name(self, missing=''): + """ returns camera name recorded by select Canon cameras""" + try: + return self['Exif.Canon.OwnerName'].value.strip() + except: + return missing + + def short_camera_model(self, includeCharacters = '', missing=''): + """ + Returns in shorterned string format the camera model used to record the image. + + Returns missing if the metadata value is not present. + + The short format is determined by the first occurrence of a digit in the + camera model, including all alphaNumeric characters before and after + that digit up till a non-alphanumeric character, but with these interventions: + + 1. Canon "Mark" designations are shortened prior to conversion. + 2. Names like "Canon EOS DIGITAL REBEL XSi" do not have a number and must + and treated differently (see below) + + Examples: + Canon EOS 300D DIGITAL -> 300D + Canon EOS 5D -> 5D + Canon EOS 5D Mark II -> 5DMkII + NIKON D2X -> D2X + NIKON D70 -> D70 + X100,D540Z,C310Z -> X100 + Canon EOS DIGITAL REBEL XSi -> XSi + Canon EOS Digital Rebel XS -> XS + Canon EOS Digital Rebel XTi -> XTi + Canon EOS Kiss Digital X -> Digital + Canon EOS Digital Rebel XT -> XT + EOS Kiss Digital -> Digital + Canon Digital IXUS Wireless -> Wireless + Canon Digital IXUS i zoom -> zoom + Canon EOS Kiss Digital N -> N + Canon Digital IXUS IIs -> IIs + IXY Digital L -> L + Digital IXUS i -> i + IXY Digital -> Digital + Digital IXUS -> IXUS + + The optional includeCharacters allows additional characters to appear + before and after the digits. + Note: special includeCharacters MUST be escaped as per syntax of a + regular expressions (see documentation for module re) + + Examples: + + includeCharacters = '': + DSC-P92 -> P92 + includeCharacters = '\-': + DSC-P92 -> DSC-P92 + + If a digit is not found in the camera model, the last word is returned. + + Note: assume exif values are in ENGLISH, regardless of current platform + """ + m = self.camera_model() + m = m.replace(' Mark ', 'Mk') + if m: + s = r"(?:[^a-zA-Z0-9%s]?)(?P[a-zA-Z0-9%s]*\d+[a-zA-Z0-9%s]*)"\ + % (includeCharacters, includeCharacters, includeCharacters) + r = re.search(s, m) + if r: + return r.group("model") + else: + head, space, model = m.strip().rpartition(' ') + return model + else: + return missing + + + def date_time(self, missing=''): + """ + Returns in python datetime format the date and time the image was + recorded. + + Trys to get value from exif key "Exif.Photo.DateTimeOriginal". + If that does not exist, trys key "Exif.Image.DateTime" + + Returns missing either metadata value is not present. + """ + try: + if "Exif.Photo.DateTimeOriginal" in self.exif_keys: + v = self["Exif.Photo.DateTimeOriginal"].value + else: + v = self["Exif.Image.DateTime"].value + + return v + except: + return missing + + def time_stamp(self, missing=''): + dt = self.date_time(missing=None) + if not dt is None: + try: + t = dt.timetuple() + ts = time.mktime(t) + except: + ts = missing + else: + ts = missing + return ts + + def sub_seconds(self, missing='00'): + """ returns the subsecond the image was taken, as recorded by the camera""" + try: + return str(self["Exif.Photo.SubSecTimeOriginal"].value) + except: + return missing + + def orientation(self, missing=''): + """ + Returns the orientation of the image, as recorded by the camera + Return type int + """ + try: + v = self['Exif.Image.Orientation'].value + if isinstance(v, types.StringType): + v = int(v) + return v + except: + return missing + + +class DummyMetaData(MetaData): + """ + Class which gives metadata values for an imaginary image. + + Useful for displaying in preference examples etc. when no image is ready to + be downloaded. + + See MetaData class for documentation of class methods. + """ + + def __init__(self): + pass + + def readMetadata(self): + pass + + def aperture(self, missing=''): + return "2.0" + + def iso(self, missing=''): + return "100" + + def exposure_time(self, alternativeFormat=False, missing=''): + if alternativeFormat: + return "4000" + else: + return "1/4000" + + def focal_length(self, missing=''): + return "135" + + def camera_make(self, missing=''): + return "Canon" + + def camera_model(self, missing=''): + return "Canon EOS 5D" + + def short_camera_model(self, includeCharacters = '', missing=''): + return "5D" + + def camera_serial(self, missing=''): + return '730402168' + + def shutter_count(self, missing=''): + return '387' + + def owner_name(self, missing=''): + return 'Photographer Name' + + def date_time(self, missing=''): + return datetime.datetime.now() + + def subSeconds(self, missing='00'): + return '57' + + def orientation(self, missing=''): + return 1 + +if __name__ == '__main__': + import sys + + + if (len(sys.argv) != 2): + print 'Usage: ' + sys.argv[0] + ' path/to/photo/containing/metadata' + m = DummyMetaData() + + else: + m = MetaData(sys.argv[1]) + m.read() + + print "f"+ m.aperture('missing ') + print "ISO " + m.iso('missing ') + print m.exposure_time(missing='missing ') + " sec" + print m.exposure_time(alternativeFormat=True, missing='missing ') + print m.focal_length('missing ') + "mm" + print m.camera_make() + print m.camera_model() + print m.short_camera_model() + print m.short_camera_model(includeCharacters = "\-") + print m.date_time() + print m.orientation() + print 'Serial number:', m.camera_serial() + print 'Shutter count:', m.shutter_count() + print 'Subseconds:', m.sub_seconds() + diff --git a/rapid/metadatavideo.py b/rapid/metadatavideo.py new file mode 100644 index 0000000..144686a --- /dev/null +++ b/rapid/metadatavideo.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007-11 Damon Lynch + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +DOWNLOAD_VIDEO = True + +import os +import datetime +import time +import subprocess +import tempfile + +import multiprocessing +import logging +logger = multiprocessing.get_logger() + +import gtk +import paths + +import rpdfile + +try: + from hachoir_core.cmd_line import unicodeFilename + from hachoir_parser import createParser + from hachoir_metadata import extractMetadata +except ImportError: + DOWNLOAD_VIDEO = False + + +if DOWNLOAD_VIDEO: + + def version_info(): + from hachoir_metadata.version import VERSION + return VERSION + + def get_video_THM_file(full_filename): + """ + Checks to see if a thumbnail file (THM) is in the same directory as the + file. Expects a full path to be part of the file name. + + Returns the filename, including path, if found, else returns None. + """ + + f = None + name, ext = os.path.splitext(full_filename) + for e in rpdfile.VIDEO_THUMBNAIL_FILE_EXTENSIONS: + if os.path.exists(name + '.' + e): + f = name + '.' + e + break + if os.path.exists(name + '.' + e.upper()): + f = name + '.' + e.upper() + break + + return f + + class VideoMetaData(): + def __init__(self, filename): + """ + Initialize by loading metadata using hachoir + """ + + self.filename = filename + self.u_filename = unicodeFilename(filename) + self.parser = createParser(self.u_filename, self.filename) + self.metadata = extractMetadata(self.parser) + + + def _kaa_get(self, key, missing, stream=None): + + if not hasattr(self, 'info'): + try: + from kaa.metadata import parse + except ImportError: + msg = """The package Kaa metadata does not exist. +It is needed to access FPS and codec video file metadata.""" + logger.error(msg) + self.info = None + else: + self.info = parse(self.filename) + if self.info: + if stream != None: + v = self.info['video'][stream][key] + else: + v = self.info[key] + else: + v = None + if v: + return str(v) + else: + return missing + + def _get(self, key, missing): + try: + v = self.metadata.get(key) + except: + v = missing + return v + + def date_time(self, missing=''): + return self._get('creation_date', missing) + + def time_stamp(self, missing=''): + """ + Returns a float value representing the time stamp, if it exists + """ + dt = self.date_time(missing=None) + if dt: + # convert it to a time stamp (not optimal, but better than nothing!) + v = time.mktime(dt.timetuple()) + else: + v = missing + return v + + def codec(self, stream=0, missing=''): + return self._kaa_get('codec', missing, stream) + + def length(self, missing=''): + """ + return the duration (length) of the video, rounded to the nearest second, in string format + """ + delta = self.metadata.get('duration') + l = '%.0f' % (86400 * delta.days + delta.seconds + float('.%s' % delta.microseconds)) + return l + + + def width(self, missing=''): + v = self._get('width', missing) + if v != None: + return str(v) + else: + return None + + def height(self, missing=''): + v = self._get('height', missing) + if v != None: + return str(v) + else: + return None + + def frames_per_second(self, stream=0, missing=''): + fps = self._kaa_get('fps', missing, stream) + try: + fps = '%.0f' % float(fps) + except: + pass + return fps + + def fourcc(self, stream=0, missing=''): + return self._kaa_get('fourcc', missing, stream) + + + +class DummyMetaData(): + """ + Class which gives metadata values for an imaginary video. + + Useful for displaying in preference examples etc. when no video is ready to + be downloaded. + + See VideoMetaData class for documentation of class methods. + """ + def __init__(self): + pass + + def date_time(self, missing=''): + return date_time.date_time.now() + + def codec(self, stream=0, missing=''): + return 'H.264 AVC' + + def length(self, missing=''): + return '57' + + def width(self, stream=0, missing=''): + return '1920' + + def height(self, stream=0, missing=''): + return '1080' + + def frames_per_second(self, stream=0, missing=''): + return '24' + + def fourcc(self, stream=0, missing=''): + return 'AVC1' + + +if __name__ == '__main__': + import sys + + + if (len(sys.argv) != 2): + print 'Usage: ' + sys.argv[0] + ' path/to/video/containing/metadata' + sys.exit(0) + + else: + m = VideoMetaData(sys.argv[1]) + dt = m.date_time() + if dt: + print dt.strftime('%Y%m%d-%H:%M:%S') + print "codec: %s" % m.codec() + print "%s seconds" % m.length() + print "%sx%s" % (m.width(), m.height()) + print "%s fps" % m.frames_per_second() + print "Fourcc: %s" % (m.fourcc()) + diff --git a/rapid/preferencesdialog.py b/rapid/preferencesdialog.py new file mode 100644 index 0000000..5ad257d --- /dev/null +++ b/rapid/preferencesdialog.py @@ -0,0 +1,1583 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007 - 2011 Damon Lynch + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +import datetime + +import gtk + +import multiprocessing +import logging +logger = multiprocessing.get_logger() + +import ValidatedEntry +import misc + +import config +import paths +import higdefaults as hd +import metadataphoto +import metadatavideo +import tableplusminus as tpm + +import utilities + +import generatename as gn +from generatenameconfig import * +import problemnotification as pn + +from prefsrapid import format_pref_list_for_pretty_print + +from gettext import gettext as _ + +class PrefError(Exception): + """ base class """ + def unpackList(self, l): + """ + Make the preferences presentable to the user + """ + + s = '' + for i in l: + if i <> ORDER_KEY: + s += "'" + i + "', " + return s[:-2] + + def __str__(self): + return self.msg + +class PrefKeyError(PrefError): + def __init__(self, error): + value = error[0] + expectedValues = self.unpackList(error[1]) + self.msg = "Preference key '%(key)s' is invalid.\nExpected one of %(value)s" % { + 'key': value, 'value': expectedValues} + + +class PrefValueInvalidError(PrefKeyError): + def __init__(self, error): + value = error[0] + self.msg = "Preference value '%(value)s' is invalid" % {'value': value} + +class PrefLengthError(PrefError): + def __init__(self, error): + self.msg = "These preferences are not well formed:" + "\n %s" % self.unpackList(error) + +class PrefValueKeyComboError(PrefError): + def __init__(self, error): + self.msg = error + + +def check_pref_valid(pref_defn, prefs, modulo=3): + """ + Checks to see if prefs are valid according to definition. + + prefs is a list of preferences. + pref_defn is a Dict specifying what is valid. + modulo is how many list elements are equivalent to one line of preferences. + + Returns True if prefs match with pref_defn, + else raises appropriate error. + """ + + if (len(prefs) % modulo <> 0) or not prefs: + raise PrefLengthError(prefs) + else: + for i in range(0, len(prefs), modulo): + _check_pref_valid(pref_defn, prefs[i:i+modulo]) + + return True + +def _check_pref_valid(pref_defn, prefs): + + key = prefs[0] + value = prefs[1] + + + if pref_defn.has_key(key): + + next_pref_defn = pref_defn[key] + + if value == None: + # value should never be None, at any time + raise PrefValueInvalidError((None, next_pref_defn)) + + if next_pref_defn and not value: + raise gn.PrefValueInvalidError((value, next_pref_defn)) + + if type(next_pref_defn) == type({}): + return _check_pref_valid(next_pref_defn, prefs[1:]) + else: + if type(next_pref_defn) == type([]): + result = value in next_pref_defn + if not result: + raise gn.PrefValueInvalidError((value, next_pref_defn)) + return True + elif not next_pref_defn: + return True + else: + result = next_pref_defn == value + if not result: + raise gn.PrefKeyValue((value, next_pref_defn)) + return True + else: + raise PrefKeyError((key, pref_defn[ORDER_KEY])) + + +def filter_subfolder_prefs(pref_list): + """ + Filters out extraneous preference choices + """ + prefs_changed = False + continue_check = True + while continue_check and pref_list: + continue_check = False + if pref_list[0] == SEPARATOR: + # subfolder preferences should not start with a / + pref_list = pref_list[3:] + prefs_changed = True + continue_check = True + elif pref_list[-3] == SEPARATOR: + # subfolder preferences should not end with a / + pref_list = pref_list[:-3] + continue_check = True + prefs_changed = True + else: + for i in range(0, len(pref_list) - 3, 3): + if pref_list[i] == SEPARATOR and pref_list[i+3] == SEPARATOR: + # subfolder preferences should not contain two /s side by side + continue_check = True + prefs_changed = True + # note we are messing with the contents of the pref list, + # must exit loop and try again + pref_list = pref_list[:i] + pref_list[i+3:] + break + + return (prefs_changed, pref_list) + +class Comboi18n(gtk.ComboBox): + """ very simple i18n version of the venerable combo box + with one column displayed to the user. + + This combo box has two columns: + 1. the first contains the actual value and is invisible + 2. the second contains the translation of the first column, and this is what + the users sees + """ + def __init__(self): + liststore = gtk.ListStore(str, str) + gtk.ComboBox.__init__(self, liststore) + cell = gtk.CellRendererText() + self.pack_start(cell, True) + self.add_attribute(cell, 'text', 1) + + def append_text(self, text): + model = self.get_model() + model.append((text, _(text))) + + def get_active_text(self): + model = self.get_model() + active = self.get_active() + if active < 0: + return None + return model[active][0] + +class PreferenceWidgets: + + def __init__(self, default_row, default_prefs, pref_defn_L0, pref_list): + self.default_row = default_row + self.default_prefs = default_prefs + self.pref_defn_L0 = pref_defn_L0 + self.pref_list = pref_list + + def _create_combo(self, choices): + combobox = Comboi18n() + for text in choices: + combobox.append_text(text) + return combobox + + def get_default_row(self): + """ + returns a list of default widgets + """ + return self.get_widgets_based_on_user_selection(self.default_row) + + def _get_pref_widgets(self, pref_definition, prefs, widgets): + key = prefs[0] + value = prefs[1] + + # supply a default value if the user has not yet chosen a value! + if not key: + key = pref_definition[ORDER_KEY][0] + + if not key in pref_definition: + raise gn.PrefKeyError((key, pref_definition.keys())) + + + list0 = pref_definition[ORDER_KEY] + + # the first widget will always be a combo box + widget0 = self._create_combo(list0) + widget0.set_active(list0.index(key)) + + widgets.append(widget0) + + if key == TEXT: + widget1 = gtk.Entry() + widget1.set_text(value) + + widgets.append(widget1) + widgets.append(None) + return + elif key in [SEPARATOR, JOB_CODE]: + widgets.append(None) + widgets.append(None) + return + else: + next_pref_definition = pref_definition[key] + if type(next_pref_definition) == type({}): + return self._get_pref_widgets(next_pref_definition, + prefs[1:], + widgets) + else: + if type(next_pref_definition) == type([]): + widget1 = self._create_combo(next_pref_definition) + if not value: + value = next_pref_definition[0] + try: + widget1.set_active(next_pref_definition.index(value)) + except: + raise gn.PrefValueInvalidError((value, next_pref_definition)) + + widgets.append(widget1) + else: + widgets.append(None) + + def _get_values_from_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]) + + def get_widgets_based_on_prefs(self): + """ + Yields a list of widgets and their callbacks based on the users preferences. + + This list is equivalent to one row of preferences when presented to the + user in the Plus Minus Table. + """ + + for L0, L1, L2 in self._get_values_from_list(): + prefs = [L0, L1, L2] + widgets = [] + self._get_pref_widgets(self.pref_defn_L0, prefs, widgets) + yield widgets + + + def get_widgets_based_on_user_selection(self, selection): + """ + Returns a list of widgets and their callbacks based on what the user has selected. + + Selection is the values the user has chosen thus far in comboboxes. + It determines the contents of the widgets returned. + It should be a list of three values, with None for values not chosen. + For values which are None, the first value in the preferences + definition is chosen. + + """ + widgets = [] + + self._get_pref_widgets(self.pref_defn_L0, selection, widgets) + return widgets + + def check_prefs_for_validity(self): + """ + Checks preferences validity + """ + + #~ print logger.debug(dir(self)) + return check_pref_valid(self.pref_defn_L0, self.pref_list) + +class PhotoNamePrefs(PreferenceWidgets): + def __init__(self, pref_list): + PreferenceWidgets.__init__(self, + default_row = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE], + default_prefs = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE], + pref_defn_L0 = DICT_IMAGE_RENAME_L0, + pref_list = pref_list) + +class VideoNamePrefs(PreferenceWidgets): + def __init__(self, pref_list): + PreferenceWidgets.__init__(self, + default_row = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE], + default_prefs = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE], + pref_defn_L0 = DICT_VIDEO_RENAME_L0, + pref_list = pref_list) + + +class PhotoSubfolderPrefs(PreferenceWidgets): + def __init__(self, pref_list): + + PreferenceWidgets.__init__(self, + default_row = [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0]], + default_prefs = DEFAULT_SUBFOLDER_PREFS, + pref_defn_L0 = DICT_SUBFOLDER_L0, + pref_list = pref_list) + + def filter_preferences(self): + filtered, pref_list = filter_subfolder_prefs(self.pref_list) + if filtered: + self.pref_list = pref_list + + def check_prefs_for_validity(self): + """ + Checks subfolder preferences validity above and beyond image name checks. + + See parent method for full description. + + Subfolders have additional requirments to that of file names. + """ + v = PreferenceWidgets.check_prefs_for_validity(self) + if v: + # peform additional checks: + # 1. do not start with a separator + # 2. do not end with a separator + # 3. do not have two separators in a row + # these three rules will ensure something else other than a + # separator is specified + L1s = [] + for i in range(0, len(self.pref_list), 3): + L1s.append(self.pref_list[i]) + + if L1s[0] == SEPARATOR: + raise PrefValueKeyComboError(_("Subfolder preferences should not start with a %s") % os.sep) + elif L1s[-1] == SEPARATOR: + raise PrefValueKeyComboError(_("Subfolder preferences should not end with a %s") % os.sep) + else: + for i in range(len(L1s) - 1): + if L1s[i] == SEPARATOR and L1s[i+1] == SEPARATOR: + raise PrefValueKeyComboError(_("Subfolder preferences should not contain two %s one after the other") % os.sep) + return v + +class VideoSubfolderPrefs(PhotoSubfolderPrefs): + def __init__(self, pref_list): + PreferenceWidgets.__init__(self, + default_row = [DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0]], + default_prefs = DEFAULT_VIDEO_SUBFOLDER_PREFS, + pref_defn_L0 = DICT_VIDEO_SUBFOLDER_L0, + pref_list = pref_list) + +class RemoveAllJobCodeDialog(gtk.Dialog): + def __init__(self, parent_window, post_choice_callback): + gtk.Dialog.__init__(self, _('Remove all Job Codes?'), None, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_NO, gtk.RESPONSE_CANCEL, + gtk.STOCK_YES, gtk.RESPONSE_OK)) + + self.post_choice_callback = post_choice_callback + self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) + + prompt_hbox = gtk.HBox() + + icontheme = gtk.icon_theme_get_default() + icon = icontheme.load_icon('gtk-dialog-question', 36, gtk.ICON_LOOKUP_USE_BUILTIN) + if icon: + image = gtk.Image() + image.set_from_pixbuf(icon) + prompt_hbox.pack_start(image, False, False, padding = 6) + + prompt_label = gtk.Label(_('Should all Job Codes be removed?')) + prompt_label.set_line_wrap(True) + prompt_hbox.pack_start(prompt_label, False, False, padding=6) + + self.vbox.pack_start(prompt_hbox, padding=6) + + self.set_border_width(6) + self.set_has_separator(False) + + self.set_default_response(gtk.RESPONSE_OK) + + + self.set_transient_for(parent_window) + self.show_all() + + + self.connect('response', self.on_response) + + def on_response(self, device_dialog, response): + user_selected = response == gtk.RESPONSE_OK + self.post_choice_callback(self, user_selected) + +class PhotoRenameTable(tpm.TablePlusMinus): + + def __init__(self, preferencesdialog, adjust_scroll_window): + + tpm.TablePlusMinus.__init__(self, 1, 3) + self.preferencesdialog = preferencesdialog + self.adjust_scroll_window = adjust_scroll_window + if not hasattr(self, "error_title"): + self.error_title = _("Error in Photo Rename preferences") + + self.table_type = self.error_title[len("Error in "):] + self.i = 0 + + if adjust_scroll_window: + self.scroll_bar = self.adjust_scroll_window.get_vscrollbar() + #this next line does not work on early versions of pygtk :( + self.scroll_bar.connect('visibility-notify-event', self.scrollbar_visibility_change) + self.connect("size-request", self.size_adjustment) + self.connect("add", self.size_adjustment) + self.connect("remove", self.size_adjustment) + + # get scrollbar thickness from parent app scrollbar - very hackish, but what to do?? + self.bump = 16# self.preferencesdialog.parentApp.image_scrolledwindow.get_hscrollbar().allocation.height + self.have_vertical_scrollbar = False + + + self.get_preferencesdialog_prefs() + self.get_prefs_factory() + #~ self.prefs_factory.setDownloadStartTime(datetime.datetime.now()) + + try: + self.prefs_factory.check_prefs_for_validity() + + except (PrefValueInvalidError, PrefLengthError, + PrefValueKeyComboError, PrefKeyError), e: + + logger.error(self.error_title) + logger.error("Sorry, these preferences contain an error:") + logger.error(format_pref_list_for_pretty_print(self.prefs_factory.pref_list)) + + # the preferences were invalid + # reset them to their default + + self.pref_list = self.prefs_factory.default_prefs + self.get_prefs_factory() + self.update_parentapp_prefs() + + msg = "%s.\n" % e + msg += "Resetting to default values." + logger.error(msg) + + + misc.run_dialog(self.error_title, msg, + preferencesdialog, + gtk.MESSAGE_ERROR) + + for row in self.prefs_factory.get_widgets_based_on_prefs(): + self.append(row) + + def update_preferences(self): + pref_list = [] + for row in self.pm_rows: + for col in range(self.pm_no_columns): + widget = row[col] + if widget: + name = widget.get_name() + if name == 'GtkComboBox': + value = widget.get_active_text() + elif name == 'GtkEntry': + value = widget.get_text() + else: + logger.critical("Program error: Unknown preference widget!") + value = '' + else: + value = '' + pref_list.append(value) + + self.pref_list = pref_list + self.update_parentapp_prefs() + self.prefs_factory.pref_list = pref_list + self.update_example() + + + def scrollbar_visibility_change(self, widget, event): + if event.state == gtk.gdk.VISIBILITY_UNOBSCURED: + self.have_vertical_scrollbar = True + self.adjust_scroll_window.set_size_request(self.adjust_scroll_window.allocation.width + self.bump, -1) + + + def size_adjustment(self, widget, arg2): + """ + Adjust scrolledwindow width in preferences dialog to reflect width of image rename table + + The algorithm is complicated by the need to take into account the presence of a vertical scrollbar, + which might be added as the user adds more rows + + The pygtk code behaves inconsistently depending on the pygtk version + """ + + if self.adjust_scroll_window: + self.have_vertical_scrollbar = self.scroll_bar.allocation.width > 1 or self.have_vertical_scrollbar + if not self.have_vertical_scrollbar: + if self.allocation.width > self.adjust_scroll_window.allocation.width: + self.adjust_scroll_window.set_size_request(self.allocation.width, -1) + else: + if self.allocation.width > self.adjust_scroll_window.allocation.width - self.bump: + self.adjust_scroll_window.set_size_request(self.allocation.width + self.bump, -1) + self.bump = 0 + + def get_preferencesdialog_prefs(self): + self.pref_list = self.preferencesdialog.prefs.image_rename + + + def get_prefs_factory(self): + self.prefs_factory = PhotoNamePrefs(self.pref_list) + + def update_parentapp_prefs(self): + self.preferencesdialog.prefs.image_rename = self.pref_list + + def update_example_job_code(self): + job_code = self.preferencesdialog.prefs.get_sample_job_code() + if not job_code: + job_code = _('Job code') + #~ self.prefs_factory.setJobCode(job_code) + + def update_example(self): + self.preferencesdialog.update_image_rename_example() + + def get_default_row(self): + return self.prefs_factory.get_default_row() + + def on_combobox_changed(self, widget, row_position): + + for col in range(self.pm_no_columns): + if self.pm_rows[row_position][col] == widget: + break + selection = [] + for i in range(col + 1): + # ensure it is a combo box we are getting the value from + w = self.pm_rows[row_position][i] + name = w.get_name() + if name == 'GtkComboBox': + selection.append(w.get_active_text()) + else: + selection.append(w.get_text()) + + for i in range(col + 1, self.pm_no_columns): + selection.append('') + + if col <> (self.pm_no_columns - 1): + widgets = self.prefs_factory.get_widgets_based_on_user_selection(selection) + + for i in range(col + 1, self.pm_no_columns): + old_widget = self.pm_rows[row_position][i] + if old_widget: + self.remove(old_widget) + if old_widget in self.pm_callbacks: + del self.pm_callbacks[old_widget] + new_widget = widgets[i] + self.pm_rows[row_position][i] = new_widget + if new_widget: + self._create_callback(new_widget, row_position) + self.attach(new_widget, i, i+1, row_position, row_position + 1) + new_widget.show() + self.update_preferences() + + + def on_entry_changed(self, widget, row_position): + self.update_preferences() + + def on_row_added(self, row_position): + """ + Update preferences, as a row has been added + """ + self.update_preferences() + + # if this was the last row or 2nd to last row, and another has just been added, move vertical scrollbar down + if row_position in range(self.pm_no_rows - 3, self.pm_no_rows - 2): + adjustment = self.preferencesdialog.rename_scrolledwindow.get_vadjustment() + adjustment.set_value(adjustment.upper) + + + def on_row_deleted(self, row_position): + """ + Update preferences, as a row has been deleted + """ + self.update_preferences() + +class VideoRenameTable(PhotoRenameTable): + def __init__(self, preferencesdialog, adjust_scroll_window): + self.error_title = _("Error in Video Rename preferences") + PhotoRenameTable.__init__(self, preferencesdialog, adjust_scroll_window) + + def get_preferencesdialog_prefs(self): + self.pref_list = self.preferencesdialog.prefs.video_rename + + def get_prefs_factory(self): + self.prefs_factory = VideoNamePrefs(self.pref_list) + + def update_parentapp_prefs(self): + self.preferencesdialog.prefs.video_rename = self.pref_list + + def update_example(self): + self.preferencesdialog.update_video_rename_example() + +class SubfolderTable(PhotoRenameTable): + """ + Table to display photo download subfolder preferences as part of preferences + dialog window. + """ + def __init__(self, preferencesdialog, adjust_scroll_window): + self.error_title = _("Error in Photo Download Subfolders preferences") + PhotoRenameTable.__init__(self, preferencesdialog, adjust_scroll_window) + + def get_preferencesdialog_prefs(self): + self.pref_list = self.preferencesdialog.prefs.subfolder + + def get_prefs_factory(self): + self.prefs_factory = PhotoSubfolderPrefs(self.pref_list) + + def update_parentapp_prefs(self): + self.preferencesdialog.prefs.subfolder = self.pref_list + + def update_example(self): + self.preferencesdialog.update_photo_download_folder_example() + +class VideoSubfolderTable(PhotoRenameTable): + def __init__(self, preferencesdialog, adjust_scroll_window): + self.error_title = _("Error in Video Download Subfolders preferences") + PhotoRenameTable.__init__(self, preferencesdialog, adjust_scroll_window) + + def get_preferencesdialog_prefs(self): + self.pref_list = self.preferencesdialog.prefs.video_subfolder + + def get_prefs_factory(self): + self.prefs_factory = VideoSubfolderPrefs(self.pref_list) + + def update_parentapp_prefs(self): + self.preferencesdialog.prefs.video_subfolder = self.pref_list + + def update_example(self): + self.preferencesdialog.update_video_download_folder_example() + +class RemoveAllJobCodeDialog(gtk.Dialog): + def __init__(self, parent_window, post_choice_callback): + gtk.Dialog.__init__(self, _('Remove all Job Codes?'), None, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_NO, gtk.RESPONSE_CANCEL, + gtk.STOCK_YES, gtk.RESPONSE_OK)) + + self.post_choice_callback = post_choice_callback + self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) + + prompt_hbox = gtk.HBox() + + icontheme = gtk.icon_theme_get_default() + icon = icontheme.load_icon('gtk-dialog-question', 36, gtk.ICON_LOOKUP_USE_BUILTIN) + if icon: + image = gtk.Image() + image.set_from_pixbuf(icon) + prompt_hbox.pack_start(image, False, False, padding = 6) + + prompt_label = gtk.Label(_('Should all Job Codes be removed?')) + prompt_label.set_line_wrap(True) + prompt_hbox.pack_start(prompt_label, False, False, padding=6) + + self.vbox.pack_start(prompt_hbox, padding=6) + + self.set_border_width(6) + self.set_has_separator(False) + + self.set_default_response(gtk.RESPONSE_OK) + + self.set_transient_for(parent_window) + self.show_all() + + self.connect('response', self.on_response) + + def on_response(self, device_dialog, response): + user_selected = response == gtk.RESPONSE_OK + self.post_choice_callback(self, user_selected) + +class JobCodeDialog(gtk.Dialog): + """ Dialog prompting for a job code""" + + def __init__(self, parent_window, job_codes, default_job_code, post_job_code_entry_callback, entry_only): + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode + gtk.Dialog.__init__(self, _('Enter a Job Code'), None, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OK, gtk.RESPONSE_OK)) + + + self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) + self.post_job_code_entry_callback = post_job_code_entry_callback + + self.combobox = gtk.combo_box_entry_new_text() + for text in job_codes: + self.combobox.append_text(text) + + self.job_code_hbox = gtk.HBox(homogeneous = False) + + if len(job_codes) and not entry_only: + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode + task_label = gtk.Label(_('Enter a new Job Code, or select a previous one')) + else: + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode + task_label = gtk.Label(_('Enter a new Job Code')) + task_label.set_line_wrap(True) + task_hbox = gtk.HBox() + task_hbox.pack_start(task_label, False, False, padding=6) + + label = gtk.Label(_('Job Code:')) + self.job_code_hbox.pack_start(label, False, False, padding=6) + self.job_code_hbox.pack_start(self.combobox, True, True, padding=6) + + self.set_border_width(6) + self.set_has_separator(False) + + # make entry box have entry completion + self.entry = self.combobox.child + + completion = gtk.EntryCompletion() + completion.set_match_func(self.match_func) + completion.connect("match-selected", + self.on_completion_match) + completion.set_model(self.combobox.get_model()) + completion.set_text_column(0) + self.entry.set_completion(completion) + + # when user hits enter, close the dialog window + self.set_default_response(gtk.RESPONSE_OK) + self.entry.set_activates_default(True) + + if default_job_code: + self.entry.set_text(default_job_code) + + self.vbox.pack_start(task_hbox, False, False, padding = 6) + self.vbox.pack_start(self.job_code_hbox, False, False, padding=12) + + self.set_transient_for(parent_window) + self.show_all() + self.connect('response', self.on_job_code_resp) + + def match_func(self, completion, key, iter): + model = completion.get_model() + return model[iter][0].lower().startswith(self.entry.get_text().lower()) + + def on_completion_match(self, completion, model, iter): + self.entry.set_text(model[iter][0]) + self.entry.set_position(-1) + + def get_job_code(self): + return self.combobox.child.get_text() + + def on_job_code_resp(self, jc_dialog, response): + user_chose_code = False + if response == gtk.RESPONSE_OK: + user_chose_code = True + logger.debug("Job Code entered") + else: + logger.debug("Job Code not entered") + self.post_job_code_entry_callback(self, user_chose_code, self.get_job_code()) + + +class PreferencesDialog(gtk.Window): + """ + Dialog window to show Rapid Photo Downloader preferences. + + Is tightly integrated into main Rapid Photo Downloader window, i.e. + directly access members in class RapidApp. + """ + + def __init__(self, rapidapp): + + self.builder = gtk.Builder() + self.builder.add_from_file(paths.share_dir("glade3/prefs.ui")) + self.builder.connect_signals(self) + + self.dialog = self.preferencesdialog + self.widget = self.dialog + self.dialog.set_transient_for(rapidapp.rapidapp) + self.prefs = rapidapp.prefs + + rapidapp.preferences_dialog_displayed = True + + self.rapidapp = rapidapp + + self._setup_tab_selector() + + self._setup_control_spacing() + + if metadatavideo.DOWNLOAD_VIDEO: + self.file_types = _("photos and videos") + else: + self.file_types = _("photos") + + # get example photo and video data + #~ try: + #~ w = workers.firstWorkerReadyToDownload() + #~ mediaFile = w.firstImage() + #~ self.sample_image_name = mediaFile.name + #~ # assume the metadata is already read + #~ self.sample_image = mediaFile.metadata + #~ except: + self.sample_image = metadataphoto.DummyMetaData() + self.sample_image_name = 'IMG_0524.CR2' + + if not metadatavideo.DOWNLOAD_VIDEO: + self.sample_video = None + self.sample_video_name = '' + else: + #~ try: + #~ mediaFile = w.firstVideo() + #~ self.sample_video_name = mediaFile.name + #~ self.sample_video = mediaFile.metadata + #~ self.videoFallBackDate = mediaFile.modificationTime + #~ except: + self.sample_video = metadatavideo.DummyMetaData() + self.sample_video_name = 'MVI_1379.MOV' + #~ self.videoFallBackDate = datetime.datetime.now() + + + # setup tabs + self._setup_photo_download_folder_tab() + self._setup_image_rename_tab() + self._setup_video_download_folder_tab() + self._setup_video_rename_tab() + self._setup_rename_options_tab() + self._setup_job_code_tab() + self._setup_device_tab() + self._setup_backup_tab() + self._setup_miscellaneous_tab() + self._setup_error_tab() + + if not metadatavideo.DOWNLOAD_VIDEO: + self.disable_video_controls() + + self.dialog.realize() + + #set the width of the left column for selecting values + #note: this must be called after self.dialog.realize(), or else the width calculation will fail + width_of_widest_sel_row = self.treeview.get_background_area(1, self.treeview_column)[2] + self.scrolled_window.set_size_request(width_of_widest_sel_row + 2, -1) + + #set the minimum width of the scolled window holding the photo rename table + if self.rename_scrolledwindow.get_vscrollbar(): + extra = self.rename_scrolledwindow.get_vscrollbar().allocation.width + 10 + else: + extra = 10 + self.rename_scrolledwindow.set_size_request(self.rename_table.allocation.width + extra, -1) + + self.dialog.show() + + def __getattr__(self, key): + """Allow builder widgets to be accessed as self.widgetname + """ + widget = self.builder.get_object(key) + if widget: # cache lookups + setattr(self, key, widget) + return widget + raise AttributeError(key) + + def on_preferencesdialog_destroy(self, widget): + """ Delete variables from memory that cause a file descriptor to be created on a mounted media""" + del self.sample_image, self.rename_table.prefs_factory, self.subfolder_table.prefs_factory + + def _setup_tab_selector(self): + self.notebook.set_show_tabs(0) + self.model = gtk.ListStore(type("")) + column = gtk.TreeViewColumn() + rentext = gtk.CellRendererText() + column.pack_start(rentext, expand=0) + column.set_attributes(rentext, text=0) + self.treeview_column = column + self.treeview.append_column(column) + self.treeview.props.model = self.model + for c in self.notebook.get_children(): + label = self.notebook.get_tab_label(c).get_text() + if not label.startswith("_"): + self.model.append( (label,) ) + + + # select the first value in the list store + self.treeview.set_cursor(0,column) + + def on_download_folder_filechooser_button_selection_changed(self, widget): + self.prefs.download_folder = widget.get_current_folder() + self.update_photo_download_folder_example() + + def on_video_download_folder_filechooser_button_selection_changed(self, widget): + self.prefs.video_download_folder = widget.get_current_folder() + self.update_video_download_folder_example() + + def on_backup_folder_filechooser_button_selection_changed(self, widget): + self.prefs.backup_location = widget.get_current_folder() + self.update_backup_example() + + def on_device_location_filechooser_button_selection_changed(self, widget): + self.prefs.device_location = widget.get_current_folder() + + def _setup_control_spacing(self): + """ + set spacing of some but not all controls + """ + + self._setup_table_spacing(self.download_folder_table) + self._setup_table_spacing(self.video_download_folder_table) + self.download_folder_table.set_row_spacing(2, + hd.VERTICAL_CONTROL_SPACE) + self.video_download_folder_table.set_row_spacing(2, + hd.VERTICAL_CONTROL_SPACE) + self._setup_table_spacing(self.rename_example_table) + self._setup_table_spacing(self.video_rename_example_table) + self.devices_table.set_col_spacing(0, hd.NESTED_CONTROLS_SPACE) + self.automation_table.set_col_spacing(0, hd.NESTED_CONTROLS_SPACE) + + self._setup_table_spacing(self.backup_table) + self.backup_table.set_col_spacing(1, hd.NESTED_CONTROLS_SPACE) + self.backup_table.set_col_spacing(2, hd.CONTROL_LABEL_SPACE) + self._setup_table_spacing(self.compatibility_table) + self.compatibility_table.set_row_spacing(0, + hd.VERTICAL_CONTROL_LABEL_SPACE) + self._setup_table_spacing(self.error_table) + + + def _setup_table_spacing(self, table): + table.set_col_spacing(0, hd.NESTED_CONTROLS_SPACE) + table.set_col_spacing(1, hd.CONTROL_LABEL_SPACE) + + def _setup_subfolder_table(self): + self.subfolder_table = SubfolderTable(self, None) + self.subfolder_vbox.pack_start(self.subfolder_table) + self.subfolder_table.show_all() + + def _setup_video_subfolder_table(self): + self.video_subfolder_table = VideoSubfolderTable(self, None) + self.video_subfolder_vbox.pack_start(self.video_subfolder_table) + self.video_subfolder_table.show_all() + + def _setup_photo_download_folder_tab(self): + self.download_folder_filechooser_button = gtk.FileChooserButton( + _("Select a folder to download photos to")) + self.download_folder_filechooser_button.set_current_folder( + self.prefs.download_folder) + self.download_folder_filechooser_button.set_action( + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + self.download_folder_filechooser_button.connect("selection-changed", + self.on_download_folder_filechooser_button_selection_changed) + + self.download_folder_table.attach( + self.download_folder_filechooser_button, + 2, 3, 2, 3, yoptions = gtk.SHRINK) + self.download_folder_filechooser_button.show() + + self._setup_subfolder_table() + self.update_photo_download_folder_example() + + def _setup_video_download_folder_tab(self): + self.video_download_folder_filechooser_button = gtk.FileChooserButton( + _("Select a folder to download videos to")) + self.video_download_folder_filechooser_button.set_current_folder( + self.prefs.video_download_folder) + self.video_download_folder_filechooser_button.set_action( + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + self.video_download_folder_filechooser_button.connect("selection-changed", + self.on_video_download_folder_filechooser_button_selection_changed) + + self.video_download_folder_table.attach( + self.video_download_folder_filechooser_button, + 2, 3, 2, 3, yoptions = gtk.SHRINK) + self.video_download_folder_filechooser_button.show() + self._setup_video_subfolder_table() + self.update_video_download_folder_example() + + def _setup_image_rename_tab(self): + + self.rename_table = PhotoRenameTable(self, self.rename_scrolledwindow) + self.rename_table_vbox.pack_start(self.rename_table) + self.rename_table.show_all() + self.original_name_label.set_markup("%s" % self.sample_image_name) + self.update_image_rename_example() + + def _setup_video_rename_tab(self): + + self.video_rename_table = VideoRenameTable(self, self.video_rename_scrolledwindow) + self.video_rename_table_vbox.pack_start(self.video_rename_table) + self.video_rename_table.show_all() + self.video_original_name_label.set_markup("%s" % self.sample_video_name) + self.update_video_rename_example() + + def _setup_rename_options_tab(self): + + # sequence numbers + self.downloads_today_entry = ValidatedEntry.ValidatedEntry(ValidatedEntry.bounded(ValidatedEntry.v_int, int, 0)) + self.stored_number_entry = ValidatedEntry.ValidatedEntry(ValidatedEntry.bounded(ValidatedEntry.v_int, int, 1)) + self.downloads_today_entry.connect('changed', self.on_downloads_today_entry_changed) + self.stored_number_entry.connect('changed', self.on_stored_number_entry_changed) + v = self.rapidapp.downloads_today_tracker.get_and_maybe_reset_downloads_today() + self.downloads_today_entry.set_text(str(v)) + # make the displayed value of stored sequence no 1 more than actual value + # so as not to confuse the user + self.stored_number_entry.set_text(str(self.prefs.stored_sequence_no+1)) + self.sequence_vbox.pack_start(self.downloads_today_entry, expand=True, fill=True) + self.sequence_vbox.pack_start(self.stored_number_entry, expand=False) + self.downloads_today_entry.show() + self.stored_number_entry.show() + hour, minute = self.rapidapp.downloads_today_tracker.get_day_start() + self.hour_spinbutton.set_value(float(hour)) + self.minute_spinbutton.set_value(float(minute)) + + self.synchronize_raw_jpg_checkbutton.set_active( + self.prefs.synchronize_raw_jpg) + + #compatibility + self.strip_characters_checkbutton.set_active( + self.prefs.strip_characters) + + def _setup_job_code_tab(self): + self.job_code_liststore = gtk.ListStore(str) + column = gtk.TreeViewColumn() + rentext = gtk.CellRendererText() + rentext.connect('edited', self.on_job_code_edited) + rentext .set_property('editable', True) + + column.pack_start(rentext, expand=0) + column.set_attributes(rentext, text=0) + self.job_code_treeview_column = column + self.job_code_treeview.append_column(column) + self.job_code_treeview.props.model = self.job_code_liststore + for code in self.prefs.job_codes: + self.job_code_liststore.append((code, )) + + # set multiple selections + self.job_code_treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE) + + self.remove_all_job_code_button.set_image(gtk.image_new_from_stock( + gtk.STOCK_CLEAR, + gtk.ICON_SIZE_BUTTON)) + def _setup_device_tab(self): + + self.device_location_filechooser_button = gtk.FileChooserButton( + _("Select a folder containing %(file_types)s") % {'file_types':self.file_types}) + self.device_location_filechooser_button.set_current_folder( + self.prefs.device_location) + self.device_location_filechooser_button.set_action( + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + + self.device_location_filechooser_button.connect("selection-changed", + self.on_device_location_filechooser_button_selection_changed) + + self.devices2_table.attach(self.device_location_filechooser_button, + 1, 2, 1, 2, xoptions = gtk.EXPAND|gtk.FILL, yoptions = gtk.SHRINK) + self.device_location_filechooser_button.show() + self.autodetect_device_checkbutton.set_active( + self.prefs.device_autodetection) + self.autodetect_psd_checkbutton.set_active( + self.prefs.device_autodetection_psd) + + self.update_device_controls() + + + def _setup_backup_tab(self): + self.backup_folder_filechooser_button = gtk.FileChooserButton( + _("Select a folder in which to backup %(file_types)s") % {'file_types':self.file_types}) + self.backup_folder_filechooser_button.set_current_folder( + self.prefs.backup_location) + self.backup_folder_filechooser_button.set_action( + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + self.backup_folder_filechooser_button.connect("selection-changed", + self.on_backup_folder_filechooser_button_selection_changed) + self.backup_table.attach(self.backup_folder_filechooser_button, + 3, 4, 8, 9, yoptions = gtk.SHRINK) + self.backup_folder_filechooser_button.show() + self.backup_identifier_entry.set_text(self.prefs.backup_identifier) + self.video_backup_identifier_entry.set_text(self.prefs.video_backup_identifier) + + #setup controls for manipulating sensitivity + self._backup_controls0 = [self.auto_detect_backup_checkbutton] + self._backup_controls1 = [self.backup_identifier_explanation_label, + self.backup_identifier_label, + self.backup_identifier_entry, + self.example_backup_path_label, + self.backup_example_label,] + self._backup_controls2 = [self.backup_location_label, + self.backup_folder_filechooser_button, + self.backup_location_explanation_label] + self._backup_controls = self._backup_controls0 + self._backup_controls1 + \ + self._backup_controls2 + + self._backup_video_controls = [self.video_backup_identifier_label, + self.video_backup_identifier_entry] + + #assign values to checkbuttons only when other controls + #have been setup, because their toggle signal is activated + #when a value is assigned + + self.backup_checkbutton.set_active(self.prefs.backup_images) + self.auto_detect_backup_checkbutton.set_active( + self.prefs.backup_device_autodetection) + self.update_backup_controls() + self.update_backup_example() + + def _setup_miscellaneous_tab(self): + self.auto_startup_checkbutton.set_active( + self.prefs.auto_download_at_startup) + self.auto_insertion_checkbutton.set_active( + self.prefs.auto_download_upon_device_insertion) + self.auto_unmount_checkbutton.set_active( + self.prefs.auto_unmount) + self.auto_exit_checkbutton.set_active( + self.prefs.auto_exit) + self.auto_exit_force_checkbutton.set_active( + self.prefs.auto_exit_force) + self.auto_delete_checkbutton.set_active( + self.prefs.auto_delete) + + self.update_misc_controls() + + + def _setup_error_tab(self): + if self.prefs.download_conflict_resolution == config.SKIP_DOWNLOAD: + self.skip_download_radiobutton.set_active(True) + else: + self.add_identifier_radiobutton.set_active(True) + + if self.prefs.backup_duplicate_overwrite: + self.backup_duplicate_overwrite_radiobutton.set_active(True) + else: + self.backup_duplicate_skip_radiobutton.set_active(True) + + + def update_example_file_name(self, display_table, rename_table, sample, sampleName, example_label, fallback_date = None): + problem = pn.Problem() + name = 'file.cr2' + #~ if hasattr(self, display_table): + #~ rename_table.update_example_job_code() + #~ rename_table.prefs_factory.initializeProblem(problem) + #~ name = rename_table.prefs_factory.generateNameUsingPreferences( + #~ sample, sampleName, + #~ self.prefs.strip_characters, sequencesPreliminary=False, fallback_date=fallback_date) + #~ else: + #~ name = '' + + # since this is markup, escape it + text = "%s" % utilities.escape(name) + + if problem.has_problem(): + text += "\n" + # Translators: please do not modify or leave out html formatting tags like and . These are used to format the text the users sees + text += _("Warning: There is insufficient metadata to fully generate the name. Please use other renaming options.") + + example_label.set_markup(text) + + def update_image_rename_example(self): + """ + Displays example image name to the user + """ + self.update_example_file_name('rename_table', self.rename_table, self.sample_image, self.sample_image_name, self.new_name_label) + + + def update_video_rename_example(self): + """ + Displays example video name to the user + """ + self.update_example_file_name('video_rename_table', self.video_rename_table, self.sample_video, self.sample_video_name, self.video_new_name_label) + + def update_download_folder_example(self, display_table, subfolder_table, download_folder, sample, sampleName, example_download_path_label, subfolder_warning_label): + """ + Displays example subfolder name(s) to the user + """ + + problem = pn.Problem() + #~ if hasattr(self, display_table): + #~ subfolder_table.update_example_job_code() + #~ subfolder_table.update_example_job_code() + #~ subfolder_table.prefs_factory.initializeProblem(problem) + #~ path = subfolder_table.prefs_factory.generateNameUsingPreferences( + #~ sample, sampleName, + #~ self.prefs.strip_characters, fallback_date = fallback_date) + #~ else: + #~ path = '' + + path = 'sample' + + text = os.path.join(download_folder, path) + # since this is markup, escape it + path = utilities.escape(text) + if problem.has_problem(): + warning = _("Warning: There is insufficient metadata to fully generate subfolders. Please use other subfolder naming options." ) + else: + warning = "" + # Translators: you should not modify or leave out the %s. This is a code used by the programming language python to insert a value that thes user will see + example_download_path_label.set_markup(_("Example: %s") % text) + subfolder_warning_label.set_markup(warning) + + def update_photo_download_folder_example(self): + if hasattr(self, 'subfolder_table'): + self.update_download_folder_example('subfolder_table', self.subfolder_table, self.prefs.download_folder, self.sample_image, self.sample_image_name, self.example_photo_download_path_label, self.photo_subfolder_warning_label) + + def update_video_download_folder_example(self): + if hasattr(self, 'video_subfolder_table'): + self.update_download_folder_example('video_subfolder_table', self.video_subfolder_table, self.prefs.video_download_folder, self.sample_video, self.sample_video_name, self.example_video_download_path_label, self.video_subfolder_warning_label) + + def on_hour_spinbutton_value_changed(self, spinbutton): + hour = spinbutton.get_value_as_int() + minute = self.minute_spinbutton.get_value_as_int() + self.rapidapp.downloads_today_tracker.set_day_start(hour, minute) + self.on_downloads_today_entry_changed(self.downloads_today_entry) + + def on_minute_spinbutton_value_changed(self, spinbutton): + hour = self.hour_spinbutton.get_value_as_int() + minute = spinbutton.get_value_as_int() + self.rapidapp.downloads_today_tracker.set_day_start(hour, minute) + self.on_downloads_today_entry_changed(self.downloads_today_entry) + + def on_downloads_today_entry_changed(self, entry): + # do not update value if a download is occurring - it will mess it up! + if self.rapidapp.download_is_occurring(): + logger.info("Downloads today value not updated, as a download is currently occurring") + else: + v = entry.get_text() + try: + v = int(v) + except: + v = 0 + if v < 0: + v = 0 + self.rapidapp.downloads_today_tracker.reset_downloads_today(v) + self.rapidapp.refresh_downloads_today = True + self.update_image_rename_example() + + def on_stored_number_entry_changed(self, entry): + # do not update value if a download is occurring - it will mess it up! + if self.rapidapp.download_is_occurring(): + logger.info("Stored number value not updated, as a download is currently occurring") + else: + v = entry.get_text() + try: + # the displayed value of stored sequence no 1 more than actual value + # so as not to confuse the user + v = int(v) - 1 + except: + v = 0 + if v < 0: + v = 0 + self.prefs.stored_sequence_no = v + self.update_image_rename_example() + + def _update_subfolder_pref_on_error(self, new_pref_list): + self.prefs.subfolder = new_pref_list + + def _update_video_subfolder_pref_on_error(self, new_pref_list): + self.prefs.video_subfolder = new_pref_list + + + def check_subfolder_values_valid_on_exit(self, users_pref_list, update_pref_function, filetype, default_pref_list): + """ + Checks that the user has not entered in any inappropriate values + + If they have, filters out bad values and warns the user + """ + filtered, pref_list = filter_subfolder_prefs(users_pref_list) + if filtered: + logger.info("The %(filetype)s subfolder preferences had some unnecessary values removed.", {'filetype': filetype}) + if pref_list: + update_pref_function(pref_list) + else: + #Preferences list is now empty + msg = _("The %(filetype)s subfolder preferences entered are invalid and cannot be used.\nThey will be reset to their default values.") % {'filetype': filetype} + sys.stderr.write(msg + "\n") + misc.run_dialog(PROGRAM_NAME, msg) + update_pref_function(self.prefs.get_default(default_pref_list)) + + def on_preferencesdialog_response(self, dialog, arg): + if arg == gtk.RESPONSE_HELP: + webbrowser.open("http://www.damonlynch.net/rapid/documentation") + else: + # arg==gtk.RESPONSE_CLOSE, or the user hit the 'x' to close the window + self.prefs.backup_identifier = self.backup_identifier_entry.get_property("text") + self.prefs.video_backup_identifier = self.video_backup_identifier_entry.get_property("text") + + #check subfolder preferences for bad values + self.check_subfolder_values_valid_on_exit(self.prefs.subfolder, self._update_subfolder_pref_on_error, _("photo"), "subfolder") + self.check_subfolder_values_valid_on_exit(self.prefs.video_subfolder, self._update_video_subfolder_pref_on_error, _("video"), "video_subfolder") + + self.dialog.destroy() + self.rapidapp.preferences_dialog_displayed = False + self.rapidapp.post_preference_change() + + + + + def on_add_job_code_button_clicked(self, button): + j = JobCodeDialog(parent_window = self.dialog, + job_codes = self.prefs.job_codes, + default_job_code = None, + post_job_code_entry_callback=self.add_job_code, + entry_only = True) + + def add_job_code(self, dialog, user_chose_code, job_code): + dialog.destroy() + if user_chose_code: + if job_code and job_code not in self.prefs.job_codes: + self.job_code_liststore.prepend((job_code, )) + self.update_job_codes() + selection = self.job_code_treeview.get_selection() + selection.unselect_all() + selection.select_path((0, )) + #scroll to the top + adjustment = self.job_code_scrolledwindow.get_vadjustment() + adjustment.set_value(adjustment.lower) + + def on_remove_job_code_button_clicked(self, button): + """ remove selected job codes (can be multiple selection)""" + selection = self.job_code_treeview.get_selection() + model, selected = selection.get_selected_rows() + iters = [model.get_iter(path) for path in selected] + # only delete if a jobe code is selected + if iters: + no = len(iters) + path = None + for i in range(0, no): + iter = iters[i] + if i == no - 1: + path = model.get_path(iter) + model.remove(iter) + + # now that we removed the selection, play nice with + # the user and select the next item + selection.select_path(path) + + # if there was no selection that meant the user + # removed the last entry, so we try to select the + # last item + if not selection.path_is_selected(path): + row = path[0]-1 + # test case for empty lists + if row >= 0: + selection.select_path((row,)) + + self.update_job_codes() + self.update_image_rename_example() + self.update_video_rename_example() + self.update_photo_download_folder_example() + self.update_video_download_folder_example() + + def on_remove_all_job_code_button_clicked(self, button): + j = RemoveAllJobCodeDialog(self.dialog, self.remove_all_job_code) + + def remove_all_job_code(self, dialog, user_selected): + dialog.destroy() + if user_selected: + self.job_code_liststore.clear() + self.update_job_codes() + self.update_image_rename_example() + self.update_video_rename_example() + self.update_photo_download_folder_example() + self.update_video_download_folder_example() + + def on_job_code_edited(self, widget, path, new_text): + iter = self.job_code_liststore.get_iter(path) + self.job_code_liststore.set_value(iter, 0, new_text) + self.update_job_codes() + self.update_image_rename_example() + self.update_video_rename_example() + self.update_photo_download_folder_example() + self.update_video_download_folder_example() + + def update_job_codes(self): + """ update preferences with list of job codes""" + job_codes = [] + for row in self.job_code_liststore: + job_codes.append(row[0]) + self.prefs.job_codes = job_codes + + def on_auto_startup_checkbutton_toggled(self, checkbutton): + self.prefs.auto_download_at_startup = checkbutton.get_active() + + def on_auto_insertion_checkbutton_toggled(self, checkbutton): + self.prefs.auto_download_upon_device_insertion = checkbutton.get_active() + + def on_auto_unmount_checkbutton_toggled(self, checkbutton): + self.prefs.auto_unmount = checkbutton.get_active() + + + def on_auto_delete_checkbutton_toggled(self, checkbutton): + self.prefs.auto_delete = checkbutton.get_active() + + def on_auto_exit_checkbutton_toggled(self, checkbutton): + active = checkbutton.get_active() + self.prefs.auto_exit = active + if not active: + self.prefs.auto_exit_force = False + self.auto_exit_force_checkbutton.set_active(False) + self.update_misc_controls() + + def on_auto_exit_force_checkbutton_toggled(self, checkbutton): + self.prefs.auto_exit_force = checkbutton.get_active() + + def on_scan_metadata_checkbutton_toggled(self, checkbutton): + self.prefs.enable_previews = checkbutton.get_active() + + def on_autodetect_device_checkbutton_toggled(self, checkbutton): + self.prefs.device_autodetection = checkbutton.get_active() + self.update_device_controls() + + def on_autodetect_psd_checkbutton_toggled(self, checkbutton): + self.prefs.device_autodetection_psd = checkbutton.get_active() + + def on_backup_duplicate_overwrite_radiobutton_toggled(self, widget): + self.prefs.backup_duplicate_overwrite = widget.get_active() + + def on_backup_duplicate_skip_radiobutton_toggled(self, widget): + self.prefs.backup_duplicate_overwrite = not widget.get_active() + + def on_treeview_cursor_changed(self, tree): + path, column = tree.get_cursor() + self.notebook.set_current_page(path[0]) + + def on_synchronize_raw_jpg_checkbutton_toggled(self, check_button): + self.prefs.synchronize_raw_jpg = check_button.get_active() + + def on_strip_characters_checkbutton_toggled(self, check_button): + self.prefs.strip_characters = check_button.get_active() + self.update_image_rename_example() + self.update_photo_download_folder_example() + self.update_video_download_folder_example() + + def on_add_identifier_radiobutton_toggled(self, widget): + if widget.get_active(): + self.prefs.download_conflict_resolution = config.ADD_UNIQUE_IDENTIFIER + else: + self.prefs.download_conflict_resolution = config.SKIP_DOWNLOAD + + + def update_device_controls(self): + """ + Sets sensitivity of image device controls + """ + controls = [self.device_location_explanation_label, + self.device_location_label, + self.device_location_filechooser_button] + + if self.prefs.device_autodetection: + for c in controls: + c.set_sensitive(False) + self.autodetect_psd_checkbutton.set_sensitive(True) + self.autodetect_image_devices_label.set_sensitive(True) + else: + for c in controls: + c.set_sensitive(True) + self.autodetect_psd_checkbutton.set_sensitive(False) + self.autodetect_image_devices_label.set_sensitive(False) + + def update_misc_controls(self): + """ + Sets sensitivity of miscillaneous controls + """ + + self.auto_exit_force_checkbutton.set_sensitive(self.prefs.auto_exit) + + + def update_backup_controls(self): + """ + Sets sensitivity of backup related widgets + """ + + if not self.backup_checkbutton.get_active(): + for c in self._backup_controls + self._backup_video_controls: + c.set_sensitive(False) + + else: + for c in self._backup_controls0: + c.set_sensitive(True) + self.update_backup_controls_auto() + + def update_backup_controls_auto(self): + """ + Sets sensitivity of subset of backup related widgets + """ + + if self.auto_detect_backup_checkbutton.get_active(): + for c in self._backup_controls1: + c.set_sensitive(True) + for c in self._backup_controls2: + c.set_sensitive(False) + for c in self._backup_video_controls: + c.set_sensitive(False) + if metadatavideo.DOWNLOAD_VIDEO: + for c in self._backup_video_controls: + c.set_sensitive(True) + else: + for c in self._backup_controls1: + c.set_sensitive(False) + for c in self._backup_controls2: + c.set_sensitive(True) + if metadatavideo.DOWNLOAD_VIDEO: + for c in self._backup_video_controls: + c.set_sensitive(False) + + def disable_video_controls(self): + """ + Disables video preferences if video downloading is disabled + (probably because the appropriate libraries to enable + video metadata extraction are not installed) + """ + controls = [self.example_video_filename_label, + self.original_video_filename_label, + self.new_video_filename_label, + self.video_new_name_label, + self.video_original_name_label, + self.video_rename_scrolledwindow, + self.video_folders_hbox, + self.video_backup_identifier_label, + self.video_backup_identifier_entry + ] + for c in controls: + c.set_sensitive(False) + + self.videos_cannot_be_downloaded_label.show() + self.folder_videos_cannot_be_downloaded_label.show() + self.folder_videos_cannot_be_downloaded_hbox.show() + + def on_auto_detect_backup_checkbutton_toggled(self, widget): + self.prefs.backup_device_autodetection = widget.get_active() + self.update_backup_controls_auto() + + def on_backup_checkbutton_toggled(self, widget): + self.prefs.backup_images = self.backup_checkbutton.get_active() + self.update_backup_controls() + + def on_backup_identifier_entry_changed(self, widget): + self.update_backup_example() + #~ self.prefs. + + def on_video_backup_identifier_entry_changed(self, widget): + self.update_backup_example() + + def on_backup_scan_folder_on_entry_changed(self, widget): + self.update_backup_example() + + def update_backup_example(self): + # Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. + drive1 = os.path.join(config.MEDIA_LOCATION, _("externaldrive1")) + # Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. + drive2 = os.path.join(config.MEDIA_LOCATION, _("externaldrive2")) + + path = os.path.join(drive1, self.backup_identifier_entry.get_text()) + path2 = os.path.join(drive2, self.backup_identifier_entry.get_text()) + path3 = os.path.join(drive2, self.video_backup_identifier_entry.get_text()) + path = utilities.escape(path) + path2 = utilities.escape(path2) + path3 = utilities.escape(path3) + if metadatavideo.DOWNLOAD_VIDEO: + example = "%s\n%s\n%s" % (path, path2, path3) + else: + example = "%s\n%s" % (path, path2) + self.example_backup_path_label.set_markup(example) diff --git a/rapid/prefsrapid.py b/rapid/prefsrapid.py new file mode 100644 index 0000000..82079ec --- /dev/null +++ b/rapid/prefsrapid.py @@ -0,0 +1,395 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007, 2008, 2009, 2010, 2011 Damon Lynch + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import subprocess, os, datetime + +import prefs + +import preferencesdialog as pd +from generatenameconfig import * +import rpdfile + +import utilities +import config +__version__ = config.version + +import multiprocessing +import logging +logger = multiprocessing.get_logger() + +from gettext import gettext as _ + +def _get_default_location_legacy(options, ignore_missing_dir=False): + if ignore_missing_dir: + return utilities.get_full_path(options[0]) + for default in options: + path = utilities.get_full_path(default) + if os.path.isdir(path): + return path + return utilities.get_full_path('') + +def _get_default_location_XDG(dir_type): + proc = subprocess.Popen(['xdg-user-dir', dir_type], stdout=subprocess.PIPE) + output = proc.communicate()[0].strip() + return output + +def get_default_photo_location(ignore_missing_dir=False): + try: + return _get_default_location_XDG('PICTURES') + except: + return _get_default_location_legacy(config.DEFAULT_PHOTO_LOCATIONS, ignore_missing_dir) + +def get_default_video_location(ignore_missing_dir=False): + try: + return _get_default_location_XDG('VIDEOS') + except: + return _get_default_location_legacy(config.DEFAULT_VIDEO_LOCATIONS, ignore_missing_dir) + +def get_default_backup_photo_identifier(): + return os.path.split(get_default_photo_location(ignore_missing_dir = True))[1] + +def get_default_backup_video_identifier(): + return os.path.split(get_default_video_location(ignore_missing_dir = True))[1] + +def today(): + return datetime.date.today().strftime('%Y-%m-%d') + +class RapidPreferences(prefs.Preferences): + + defaults = { + "program_version": prefs.Value(prefs.STRING, ""), + "download_folder": prefs.Value(prefs.STRING, + get_default_photo_location()), + "video_download_folder": prefs.Value(prefs.STRING, + get_default_video_location()), + "subfolder": prefs.ListValue(prefs.STRING_LIST, DEFAULT_SUBFOLDER_PREFS), + "video_subfolder": prefs.ListValue(prefs.STRING_LIST, DEFAULT_VIDEO_SUBFOLDER_PREFS), + "image_rename": prefs.ListValue(prefs.STRING_LIST, [FILENAME, + NAME_EXTENSION, + ORIGINAL_CASE]), + "video_rename": prefs.ListValue(prefs.STRING_LIST, [FILENAME, + NAME_EXTENSION, + ORIGINAL_CASE]), + "device_autodetection": prefs.Value(prefs.BOOL, True), + "device_location": prefs.Value(prefs.STRING, os.path.expanduser('~')), + "device_autodetection_psd": prefs.Value(prefs.BOOL, False), + "device_whitelist": prefs.ListValue(prefs.STRING_LIST, ['']), + "device_blacklist": prefs.ListValue(prefs.STRING_LIST, ['']), + "backup_images": prefs.Value(prefs.BOOL, False), + "backup_device_autodetection": prefs.Value(prefs.BOOL, True), + "backup_identifier": prefs.Value(prefs.STRING, + get_default_backup_photo_identifier()), + "video_backup_identifier": prefs.Value(prefs.STRING, + get_default_backup_video_identifier()), + "backup_location": prefs.Value(prefs.STRING, os.path.expanduser('~')), + "strip_characters": prefs.Value(prefs.BOOL, True), + "auto_download_at_startup": prefs.Value(prefs.BOOL, False), + "auto_download_upon_device_insertion": prefs.Value(prefs.BOOL, False), + "auto_unmount": prefs.Value(prefs.BOOL, False), + "auto_exit": prefs.Value(prefs.BOOL, False), + "auto_exit_force": prefs.Value(prefs.BOOL, False), + "auto_delete": prefs.Value(prefs.BOOL, False), + "download_conflict_resolution": prefs.Value(prefs.STRING, + config.SKIP_DOWNLOAD), + "backup_duplicate_overwrite": prefs.Value(prefs.BOOL, False), + "display_selection": prefs.Value(prefs.BOOL, True), + "display_size_column": prefs.Value(prefs.BOOL, True), + "display_filename_column": prefs.Value(prefs.BOOL, False), + "display_type_column": prefs.Value(prefs.BOOL, True), + "display_path_column": prefs.Value(prefs.BOOL, False), + "display_device_column": prefs.Value(prefs.BOOL, False), + "display_preview_folders": prefs.Value(prefs.BOOL, True), + "show_log_dialog": prefs.Value(prefs.BOOL, False), + "day_start": prefs.Value(prefs.STRING, "03:00"), + "downloads_today": prefs.ListValue(prefs.STRING_LIST, [today(), '0']), + "stored_sequence_no": prefs.Value(prefs.INT, 0), + "job_codes": prefs.ListValue(prefs.STRING_LIST, [_('New York'), + _('Manila'), _('Prague'), _('Helsinki'), _('Wellington'), + _('Tehran'), _('Kampala'), _('Paris'), _('Berlin'), _('Sydney'), + _('Budapest'), _('Rome'), _('Moscow'), _('Delhi'), _('Warsaw'), + _('Jakarta'), _('Madrid'), _('Stockholm')]), + "synchronize_raw_jpg": prefs.Value(prefs.BOOL, False), + #~ "hpaned_pos": prefs.Value(prefs.INT, 0), + "vpaned_pos": prefs.Value(prefs.INT, 0), + "main_window_size_x": prefs.Value(prefs.INT, 0), + "main_window_size_y": prefs.Value(prefs.INT, 0), + "main_window_maximized": prefs.Value(prefs.INT, 0), + "show_warning_downloading_from_camera": prefs.Value(prefs.BOOL, True), + #~ "preview_zoom": prefs.Value(prefs.INT, zoom), + "enable_previews": prefs.Value(prefs.BOOL, True), + } + + def __init__(self): + prefs.Preferences.__init__(self, config.GCONF_KEY, self.defaults) + + + def get_downloads_today_tracker(self): + return DownloadsTodayTracker(downloads_today_date = self.downloads_today[0], + downloads_today = self.downloads_today[1], + day_start = self.day_start + ) + + def set_downloads_today_from_tracker(self, downloads_today_tracker): + self.downloads_today = downloads_today_tracker.downloads_today + self.day_start = downloads_today_tracker.day_start + + def get_sample_job_code(self): + if self.job_codes: + return self.job_codes[0] + else: + return '' + + def _get_pref_lists(self): + return (self.image_rename, self.subfolder, self.video_rename, + self.video_subfolder) + + def _pref_list_uses_component(self, pref_list, pref_component, offset): + for i in range(0, len(pref_list), 3): + if pref_list[i+offset] == pref_component: + return True + return False + + def any_pref_uses_stored_sequence_no(self): + """Returns True if any of the pref lists contain a stored sequence no""" + for pref_list in self._get_pref_lists(): + if self._pref_list_uses_component(pref_list, STORED_SEQ_NUMBER, 1): + return True + return False + + def any_pref_uses_session_sequece_no(self): + """Returns True if any of the pref lists contain a session sequence no""" + for pref_list in self._get_pref_lists(): + if self._pref_list_uses_component(pref_list, SESSION_SEQ_NUMBER, 1): + return True + return False + + def any_pref_uses_sequence_letter_value(self): + """Returns True if any of the pref lists contain a sequence letter""" + for pref_list in self._get_pref_lists(): + if self._pref_list_uses_component(pref_list, SEQUENCE_LETTER, 1): + return True + return False + + def reset(self): + """ + resets all preferences to default values + """ + + prefs.Preferences.reset(self) + self.program_version = __version__ + + + def pref_uses_job_code(self, pref_list): + """ Returns True if the particular preferences contains a job code""" + for i in range(0, len(pref_list), 3): + if pref_list[i] == JOB_CODE: + return True + return False + + def any_pref_uses_job_code(self): + """ Returns True if any of the preferences contain a job code""" + for pref_list in self._get_pref_lists(): + if self.pref_uses_job_code(pref_list): + return True + return False + + def get_pref_lists_by_file_type(self, file_type): + """ + Returns tuple of subfolder and file rename pref lists for the given + file type + """ + if file_type == rpdfile.FILE_TYPE_PHOTO: + return (self.subfolder, self.image_rename) + else: + return (self.video_subfolder, self.video_rename) + + def get_download_folder_for_file_type(self, file_type): + """ + Returns the download folder for the given file type + """ + if file_type == rpdfile.FILE_TYPE_PHOTO: + return self.download_folder + else: + return self.video_download_folder + + +class DownloadsTodayTracker: + """ + Handles tracking the number of downloads undertaken on any one day. + + When a day starts is flexible. See http://damonlynch.net/rapid/documentation/#renameoptions + """ + def __init__(self, downloads_today_date, downloads_today, day_start): + self.day_start = day_start # string + self.downloads_today = [downloads_today_date, str(downloads_today)] # two strings + + def get_and_maybe_reset_downloads_today(self): + v = self.get_downloads_today() + if v <= 0: + self.reset_downloads_today() + return v + + def get_downloads_today(self): + """Returns the preference value for the number of downloads performed today + + If value is less than zero, that means the date has changed""" + + hour, minute = self.get_day_start() + try: + adjusted_today = datetime.datetime.strptime("%s %s:%s" % (self.downloads_today[0], hour, minute), "%Y-%m-%d %H:%M") + except: + logger.critical("Failed to calculate date adjustment. Download today values appear to be corrupted: %s %s:%s", + self.downloads_today[0], hour, minute) + adjusted_today = None + + now = datetime.datetime.today() + + if adjusted_today is None: + return -1 + + if now < adjusted_today : + try: + return int(self.downloads_today[1]) + except ValueError: + logger.error("Invalid Downloads Today value. Resetting value to zero.") + self.get_downloads_today(self.downloads_today[0] , 0) + return 0 + else: + return -1 + + def get_raw_downloads_today(self): + """ + Gets value without changing it in any way, except to check for type convesion error. + If there is an error, then the value is reset + """ + try: + return int(self.downloads_today[1]) + except ValueError: + logger.critical("Downloads today value is corrupted: %s", self.downloads_today[1]) + self.downloads_today[1] = '0' + return 0 + + def set_raw_downloads_today_from_int(self, downloads_today): + self.downloads_today[1] = str(downloads_today) + + def set_raw_downloads_today_date(self, downloads_today_date): + self.downloads_today[0] = downloads_today_date + + def get_raw_downloads_today_date(self): + return self.downloads_today[0] + + def get_raw_day_start(self): + """ + Gets value without changing it in any way + """ + return self.day_start + + def get_day_start(self): + try: + t1, t2 = self.day_start.split(":") + return (int(t1), int(t2)) + except ValueError: + logger.error("'Start of day' preference value %s is corrupted. Resetting to midnight", self.day_start) + self.day_start = "0:0" + return 0, 0 + + def increment_downloads_today(self): + """ returns true if day changed """ + v = self.get_downloads_today() + if v >= 0: + self.set_downloads_today(self.downloads_today[0], v + 1) + return False + else: + self.reset_downloads_today(1) + return True + + def reset_downloads_today(self, value=0): + now = datetime.datetime.today() + hour, minute = self.get_day_start() + t = datetime.time(hour, minute) + if now.time() < t: + date = today() + else: + d = datetime.datetime.today() + datetime.timedelta(days=1) + date = d.strftime(('%Y-%m-%d')) + + self.set_downloads_today(date, value) + + def set_downloads_today(self, date, value=0): + self.downloads_today = [date, str(value)] + + def set_day_start(self, hour, minute): + self.day_start = "%s:%s" % (hour, minute) + + def log_vals(self): + logger.info("Date %s Value %s Day start %s", self.downloads_today[0], self.downloads_today[1], self.day_start) + + + +def check_prefs_for_validity(prefs): + """ + Checks preferences for validity (called at program startup) + + Returns true if the passed in preferences are valid, else returns False + """ + + try: + tests = ((prefs.image_rename, pd.PhotoNamePrefs), + (prefs.subfolder, pd.PhotoSubfolderPrefs), + (prefs.video_rename, pd.VideoNamePrefs), + (prefs.video_subfolder, pd.VideoSubfolderPrefs)) + for pref, pref_widgets in tests: + p = pref_widgets(pref) + p.check_prefs_for_validity() + except: + return False + return True + +def insert_pref_lists(prefs, rpd_file): + """ + Convenience function to insert subfolder and file rename pref_lists for + the given file type. + + Returns the modified rpd_file + """ + subfolder_pref_list, name_pref_list = prefs.get_pref_lists_by_file_type(rpd_file.file_type) + rpd_file.subfolder_pref_list = subfolder_pref_list + rpd_file.name_pref_list = name_pref_list + return rpd_file + +def format_pref_list_for_pretty_print(pref_list): + """ returns a string useful for printing the preferences""" + + v = '' + + for i in range(0, len(pref_list), 3): + if (pref_list[i+1] or pref_list[i+2]): + c = ':' + else: + c = '' + s = "%s%s " % (pref_list[i], c) + + if pref_list[i+1]: + s = "%s%s" % (s, pref_list[i+1]) + if pref_list[i+2]: + s = "%s (%s)" % (s, pref_list[i+2]) + v += s + "\n" + return v + + diff --git a/rapid/problemnotification.py b/rapid/problemnotification.py index 8dc07d0..6238847 100755 --- a/rapid/problemnotification.py +++ b/rapid/problemnotification.py @@ -19,9 +19,7 @@ import sys import types -from common import Configi18n -global _ -_ = Configi18n._ +from gettext import gettext as _ # components diff --git a/rapid/rapid.py b/rapid/rapid.py index f41c79f..efd8145 100755 --- a/rapid/rapid.py +++ b/rapid/rapid.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: latin1 -*- -### Copyright (C) 2007, 2008, 2009, 2010 Damon Lynch +### Copyright (C) 2011 Damon Lynch ### This program is free software; you can redistribute it and/or modify ### it under the terms of the GNU General Public License as published by @@ -17,19 +17,8 @@ ### along with this program; if not, write to the Free Software ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -#needed for python 2.5, unneeded for python 2.6 -from __future__ import with_statement -import sys -import os -import shutil -import time -import datetime -import atexit import tempfile -import types -import webbrowser -import operator import dbus import dbus.bus @@ -37,3638 +26,472 @@ import dbus.service from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) -from threading import Thread, Lock -from thread import error as thread_error -from thread import get_ident - -import gtk.gdk as gdk -import pango -import gobject - -try: - import gio - import glib - using_gio = True -except ImportError: - import gnomevfs - using_gio = False - -import prefs -import paths -import gnomeglade - from optparse import OptionParser -import pynotify +import gtk +import gtk.gdk as gdk -import idletube as tube +import webbrowser -import config +import sys, time, types, os, datetime -from config import STATUS_CANNOT_DOWNLOAD, STATUS_DOWNLOADED, \ - STATUS_DOWNLOADED_WITH_WARNING, \ - STATUS_DOWNLOAD_FAILED, \ - STATUS_DOWNLOAD_PENDING, \ - STATUS_BACKUP_PROBLEM, \ - STATUS_NOT_DOWNLOADED, \ - STATUS_DOWNLOAD_AND_BACKUP_FAILED, \ - STATUS_WARNING - -import common -import misc -import higdefaults as hd +import gobject, pango, cairo, array, pangocairo, gio -from media import getDefaultPhotoLocation, getDefaultVideoLocation, \ - getDefaultBackupPhotoIdentifier, \ - getDefaultBackupVideoIdentifier - -import ValidatedEntry - -from media import CardMedia +from multiprocessing import Process, Pipe, Queue, Event, Value, Array, current_process, log_to_stderr +from ctypes import c_int, c_bool, c_char -import media +import logging +logger = log_to_stderr() -import metadata -import videometadata -from videometadata import DOWNLOAD_VIDEO +# Rapid Photo Downloader modules -import renamesubfolderprefs as rn +import rpdfile + import problemnotification as pn +import thumbnail as tn +import rpdmultiprocessing as rpdmp -import tableplusminus as tpm - -__version__ = config.version - -try: - import pygtk - pygtk.require("2.0") -except: - pass -try: - import gtk - import gtk.glade -except: - sys.exit(1) - -try: - from dropshadow import image_to_pixbuf, pixbuf_to_image, DropShadow - DROP_SHADOW = True -except: - DROP_SHADOW = False - - -#~ DROP_SHADOW = False # for testing - -from common import Configi18n -global _ -_ = Configi18n._ - -#Translators: if neccessary, for guidance in how to translate this program, you may see http://damonlynch.net/translate.html -PROGRAM_NAME = _('Rapid Photo Downloader') +import preferencesdialog +import prefsrapid -TINY_SCREEN = gtk.gdk.screen_height() <= config.TINY_SCREEN_HEIGHT -#~ TINY_SCREEN = True +import tableplusminus as tpm +import generatename as gn -def today(): - return datetime.date.today().strftime('%Y-%m-%d') +from metadatavideo import DOWNLOAD_VIDEO +import metadataphoto +import metadatavideo +import scan as scan_process +import copyfiles +import subfolderfile +import device as dv +import utilities -def cmd_line(msg): - if verbose: - print msg +import config +__version__ = config.version -exiting = False +import paths -def updateDisplay(display_queue): +import gettext +gettext.bindtextdomain(config.APP_NAME) +gettext.textdomain(config.APP_NAME) - try: - if display_queue.size() != 0: - call, args = display_queue.get() - if not exiting: - call(*args) -# else do not update display - else: - sys.stderr.write("Empty display queue!\n") - return True - - except tube.EOInformation: - for w in workers.getStartedWorkers(): - w.join() +from gettext import gettext as _ - gtk.main_quit() - - return False +from utilities import format_size_for_user +from utilities import register_iconsets -class Queue(tube.Tube): - def __init__(self, maxSize = config.MAX_NO_READERS): - tube.Tube.__init__(self, maxSize) - def setMaxSize(self, maxSize): - self.maxsize = maxSize +from config import STATUS_CANNOT_DOWNLOAD, STATUS_DOWNLOADED, \ + STATUS_DOWNLOADED_WITH_WARNING, \ + STATUS_DOWNLOAD_FAILED, \ + STATUS_DOWNLOAD_PENDING, \ + STATUS_BACKUP_PROBLEM, \ + STATUS_NOT_DOWNLOADED, \ + STATUS_DOWNLOAD_AND_BACKUP_FAILED, \ + STATUS_WARNING + +DOWNLOADED = [STATUS_DOWNLOADED, STATUS_DOWNLOADED_WITH_WARNING, STATUS_BACKUP_PROBLEM] +#Translators: if neccessary, for guidance in how to translate this program, you may see http://damonlynch.net/translate.html +PROGRAM_NAME = _('Rapid Photo Downloader') +__version__ = config.version -# Module wide values - -# set up thesse variable in global name space, and initialize with proper -# values later -# this is ugly but I don't know a better way :( +def date_time_human_readable(date, with_line_break=True): + if with_line_break: + return _("%(date)s\n%(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")} + else: + return _("%(date)s %(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")} + +def time_subseconds_human_readable(date, subseconds): + return _("%(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \ + {'hour':date.strftime("%H"), + 'minute':date.strftime("%M"), + 'second':date.strftime("%S"), + 'subsecond': subseconds} -display_queue = Queue() -media_collection_treeview = selection_hbox = log_dialog = None +def date_time_subseconds_human_readable(date, subseconds): + return _("%(date)s %(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \ + {'date':date.strftime("%x"), + 'hour':date.strftime("%H"), + 'minute':date.strftime("%M"), + 'second':date.strftime("%S"), + 'subsecond': subseconds} -job_code = None -need_job_code_for_renaming = False -class ThreadManager: +class DeviceCollection(gtk.TreeView): """ - Manages the threads that actually download photos and videos + TreeView display of devices and how many files have been copied, shown + immediately under the menu in the main application window. """ - _workers = [] - - - def append(self, w): - self._workers.append(w) - - def __getitem__(self, i): - return self._workers[i] + def __init__(self, parent_app): + + self.parent_app = parent_app + # device icon & name, size of images on the device (human readable), + # copy progress (%), copy text + self.liststore = gtk.ListStore(gtk.gdk.Pixbuf, str, str, float, str) + self.map_process_to_row = {} + + gtk.TreeView.__init__(self, self.liststore) - def __len__(self): - return len(self._workers) + self.props.enable_search = False + # make it impossible to select a row + selection = self.get_selection() + selection.set_mode(gtk.SELECTION_NONE) - def disableWorker(self, thread_id): - """ - set so a worker will not run, or if it is running, make it quit and therefore complete - """ - self._workers[thread_id].manuallyDisabled = True - if self._workers[thread_id].hasStarted: - self._workers[thread_id].quit() - - else: - self._workers[thread_id].doNotStart = True + # 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() + text_renderer = gtk.CellRendererText() + text_renderer.props.ellipsize = pango.ELLIPSIZE_MIDDLE + text_renderer.set_fixed_size(160, -1) + column0.pack_start(pixbuf_renderer, expand=False) + column0.pack_start(text_renderer, expand=True) + column0.add_attribute(pixbuf_renderer, 'pixbuf', 0) + column0.add_attribute(text_renderer, 'text', 1) + self.append_column(column0) - def _isReadyToStart(self, w): - """ - Returns True if the worker is ready to start - and has not been disabled - """ - return not w.hasStarted and not w.doNotStart and not w.manuallyDisabled - def _isReadyToDownload(self, w): - return w.scanComplete and not w.downloadStarted and not w.doNotStart and w.isAlive() and not w.manuallyDisabled - - def _isScanning(self, w): - return w.isAlive() and w.hasStarted and not w.scanComplete and not w.manuallyDisabled - - def _isDownloading(self, w): - return w.downloadStarted and w.isAlive() and not w.downloadComplete + # 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) - def _isPaused(self, w): - return w.downloadStarted and not w.running and not w.downloadComplete and not w.manuallyDisabled and w.isAlive() + column2 = gtk.TreeViewColumn(_("Download Progress"), + gtk.CellRendererProgress(), + value=3, + text=4) + self.append_column(column2) + self.show_all() - def _isFinished(self, w): - """ - Returns True if the worker has finished running + def add_device(self, process_id, device, progress_bar_text = ''): - It does not signify it finished a download - """ + # add the row, and get a temporary pointer to the row + size_files = '' + progress = 0.0 + iter = self.liststore.append((device.get_icon(), + device.get_name(), + size_files, + progress, + progress_bar_text)) - return (w.hasStarted and not w.isAlive()) or w.manuallyDisabled - - def completedDownload(self, w): - return w.completedDownload + self._set_process_map(process_id, iter) + + + def update_device(self, process_id, total_size_files): + """ + Updates the size of the photos and videos on the device, displayed to the user + """ + if process_id in self.map_process_to_row: + iter = self._get_process_map(process_id) + self.liststore.set_value(iter, 2, total_size_files) + else: + logger.error("This device is unknown") - def firstWorkerReadyToStart(self): - for w in self._workers: - if self._isReadyToStart(w): - return w - return None - - def firstWorkerReadyToDownload(self): - for w in self._workers: - if self._isReadyToDownload(w): - return w - return None - - def startWorkers(self): - for w in self.getReadyToStartWorkers(): - #for some reason, very occassionally a thread that has been started shows up in this list, so must filter them out - if not w.isAlive(): - w.start() - - def quitAllWorkers(self): - global exiting - exiting = True - for w in self._workers: - w.quit() - - def getWorkers(self): - for w in self._workers: - yield w + 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] - def getNonFinishedWorkers(self): - for w in self._workers: - if not self._isFinished(w): - yield w - - def getStartedWorkers(self): - for w in self._workers: - if w.hasStarted: - yield w - - def getReadyToStartWorkers(self): - for w in self._workers: - if self._isReadyToStart(w): - yield w - - def getReadyToDownloadWorkers(self): - for w in self._workers: - if self._isReadyToDownload(w): - yield w - - def getNotDownloadingWorkers(self): - for w in self._workers: - if w.hasStarted and not w.downloadStarted: - yield w - - def getNotDownloadingAndNotFinishedWorkers(self): - for w in self._workers: - if w.hasStarted and not w.downloadStarted and not self._isFinished(w): - yield w - - - def noReadyToStartWorkers(self): - n = 0 - for w in self._workers: - if self._isReadyToStart(w): - n += 1 - return n - - def noScanningWorkers(self): - n = 0 - for w in self._workers: - if self._isScanning(w): - n += 1 - return n - - def getScanningWorkers(self): - for w in self._workers: - if self._isScanning(w): - yield w - - def scanComplete(self, threads): + def get_all_displayed_processes(self): """ - Returns True only if the list of threads have completed their scan + returns a list of the processes currently being displayed to the user """ - for thread_id in threads: - if not self[thread_id].scanComplete: - return False - return True - - def noReadyToDownloadWorkers(self): - n = 0 - for w in self._workers: - if self._isReadyToDownload(w): - n += 1 - return n - - def getRunningWorkers(self): - for w in self._workers: - if w.hasStarted and w.isAlive(): - yield w - - def getDownloadingWorkers(self): - for w in self._workers: - if self._isDownloading(w): - yield w - - def getPausedDownloadingWorkers(self): - for w in self._workers: - if self._isPaused(w): - yield w - - def getWaitingForJobCodeWorkers(self): - for w in self._workers: - if w.waitingForJobCode: - yield w - - def getAutoStartWorkers(self): - for w in self._workers: - if w.autoStart: - yield w - - def getFinishedWorkers(self): - for w in self._workers: - if self._isFinished(w): - yield w - - def noDownloadingWorkers(self): - i = 0 - for w in self._workers: - if self._isDownloading(w): - i += 1 - return i - - def noRunningWorkers(self): - i = 0 - for w in self._workers: - if w.hasStarted and w.isAlive(): - i += 1 - return i - - def noPausedWorkers(self): - i = 0 - for w in self._workers: - if self._isPaused(w): - i += 1 - return i - - def getNextThread_id(self): - return len(self._workers) - - def printWorkerStatus(self, worker=None): - if worker: - l = [worker] - else: - l = range(len(self._workers)) - for i in l: - print "\nThread %i\n=======\n" % i - w = self._workers[i] - print "Volume / source:", w.cardMedia.prettyName(limit=0) - print "Do not start:", w.doNotStart - print "Started:", w.hasStarted - print "Running:", w.running - print "Scan completed:", w.scanComplete - print "Download started:", w.downloadStarted - print "Download completed:", w.downloadComplete - print "Finished:", self._isFinished(w) - print "Alive:", w.isAlive() - print "Manually disabled:", w.manuallyDisabled, "\n" + return self.map_process_to_row.keys() - - -workers = ThreadManager() - -class RapidPreferences(prefs.Preferences): - if TINY_SCREEN: - zoom = 120 - else: - zoom = config.MIN_THUMBNAIL_SIZE * 2 - - defaults = { - "program_version": prefs.Value(prefs.STRING, ""), - "download_folder": prefs.Value(prefs.STRING, - getDefaultPhotoLocation()), - "video_download_folder": prefs.Value(prefs.STRING, - getDefaultVideoLocation()), - "subfolder": prefs.ListValue(prefs.STRING_LIST, rn.DEFAULT_SUBFOLDER_PREFS), - "video_subfolder": prefs.ListValue(prefs.STRING_LIST, rn.DEFAULT_VIDEO_SUBFOLDER_PREFS), - "image_rename": prefs.ListValue(prefs.STRING_LIST, [rn.FILENAME, - rn.NAME_EXTENSION, - rn.ORIGINAL_CASE]), - "video_rename": prefs.ListValue(prefs.STRING_LIST, [rn.FILENAME, - rn.NAME_EXTENSION, - rn.ORIGINAL_CASE]), - "device_autodetection": prefs.Value(prefs.BOOL, True), - "device_location": prefs.Value(prefs.STRING, os.path.expanduser('~')), - "device_autodetection_psd": prefs.Value(prefs.BOOL, False), - "device_whitelist": prefs.ListValue(prefs.STRING_LIST, ['']), - "device_blacklist": prefs.ListValue(prefs.STRING_LIST, ['']), - "backup_images": prefs.Value(prefs.BOOL, False), - "backup_device_autodetection": prefs.Value(prefs.BOOL, True), - "backup_identifier": prefs.Value(prefs.STRING, - getDefaultBackupPhotoIdentifier()), - "video_backup_identifier": prefs.Value(prefs.STRING, - getDefaultBackupVideoIdentifier()), - "backup_location": prefs.Value(prefs.STRING, os.path.expanduser('~')), - "strip_characters": prefs.Value(prefs.BOOL, True), - "auto_download_at_startup": prefs.Value(prefs.BOOL, False), - "auto_download_upon_device_insertion": prefs.Value(prefs.BOOL, False), - "auto_unmount": prefs.Value(prefs.BOOL, False), - "auto_exit": prefs.Value(prefs.BOOL, False), - "auto_delete": prefs.Value(prefs.BOOL, False), - "download_conflict_resolution": prefs.Value(prefs.STRING, - config.SKIP_DOWNLOAD), - "backup_duplicate_overwrite": prefs.Value(prefs.BOOL, False), - "display_selection": prefs.Value(prefs.BOOL, True), - "display_size_column": prefs.Value(prefs.BOOL, True), - "display_filename_column": prefs.Value(prefs.BOOL, False), - "display_type_column": prefs.Value(prefs.BOOL, True), - "display_path_column": prefs.Value(prefs.BOOL, False), - "display_device_column": prefs.Value(prefs.BOOL, False), - "display_preview_folders": prefs.Value(prefs.BOOL, True), - "show_log_dialog": prefs.Value(prefs.BOOL, False), - "day_start": prefs.Value(prefs.STRING, "03:00"), - "downloads_today": prefs.ListValue(prefs.STRING_LIST, [today(), '0']), - "stored_sequence_no": prefs.Value(prefs.INT, 0), - "job_codes": prefs.ListValue(prefs.STRING_LIST, [_('New York'), - _('Manila'), _('Prague'), _('Helsinki'), _('Wellington'), - _('Tehran'), _('Kampala'), _('Paris'), _('Berlin'), _('Sydney'), - _('Budapest'), _('Rome'), _('Moscow'), _('Delhi'), _('Warsaw'), - _('Jakarta'), _('Madrid'), _('Stockholm')]), - "synchronize_raw_jpg": prefs.Value(prefs.BOOL, False), - "hpaned_pos": prefs.Value(prefs.INT, 0), - "vpaned_pos": prefs.Value(prefs.INT, 0), - "main_window_size_x": prefs.Value(prefs.INT, 0), - "main_window_size_y": prefs.Value(prefs.INT, 0), - "main_window_maximized": prefs.Value(prefs.INT, 0), - "show_warning_downloading_from_camera": prefs.Value(prefs.BOOL, True), - "preview_zoom": prefs.Value(prefs.INT, zoom), - } - - def __init__(self): - prefs.Preferences.__init__(self, config.GCONF_KEY, self.defaults) - - def getAndMaybeResetDownloadsToday(self): - v = self.getDownloadsToday() - if v <= 0: - self.resetDownloadsToday() - return v - - def getDownloadsToday(self): - """Returns the preference value for the number of downloads performed today - - If value is less than zero, that means the date has changed""" - - hour, minute = self.getDayStart() - adjustedToday = datetime.datetime.strptime("%s %s:%s" % (self.downloads_today[0], hour, minute), "%Y-%m-%d %H:%M") - - now = datetime.datetime.today() - - if now < adjustedToday : - try: - return int(self.downloads_today[1]) - except ValueError: - sys.stderr.write(_("Invalid Downloads Today value.\n")) - sys.stderr.write(_("Resetting value to zero.\n")) - self.setDownloadsToday(self.downloads_today[0] , 0) - return 0 - else: - return -1 - - def setDownloadsToday(self, date, value=0): - self.downloads_today = [date, str(value)] - - def incrementDownloadsToday(self): - """ returns true if day changed """ - v = self.getDownloadsToday() - if v >= 0: - self.setDownloadsToday(self.downloads_today[0] , v + 1) - return False - else: - self.resetDownloadsToday(1) - return True - def resetDownloadsToday(self, value=0): - now = datetime.datetime.today() - hour, minute = self.getDayStart() - t = datetime.time(hour, minute) - if now.time() < t: - date = today() - else: - d = datetime.datetime.today() + datetime.timedelta(days=1) - date = d.strftime(('%Y-%m-%d')) - - self.setDownloadsToday(date, value) - - def setDayStart(self, hour, minute): - self.day_start = "%s:%s" % (hour, minute) + def _set_process_map(self, process_id, iter): + """ + convert the temporary iter into a tree reference, which is + permanent + """ - def getDayStart(self): - try: - t1, t2 = self.day_start.split(":") - return (int(t1), int(t2)) - except ValueError: - sys.stderr.write(_("'Start of day' preference value is corrupted.\n")) - sys.stderr.write(_("Resetting to midnight.\n")) - self.day_start = "0:0" - return 0, 0 - - def getSampleJobCode(self): - if self.job_codes: - return self.job_codes[0] - else: - return '' - - def reset(self): + 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): """ - resets all preferences to default values + return the tree iter for this process """ - prefs.Preferences.reset(self) - self.program_version = __version__ - -class ImageRenameTable(tpm.TablePlusMinus): - - def __init__(self, parentApp, adjustScrollWindow): - - tpm.TablePlusMinus.__init__(self, 1, 3) - self.parentApp = parentApp - self.adjustScrollWindow = adjustScrollWindow - if not hasattr(self, "errorTitle"): - self.errorTitle = _("Error in Photo Rename preferences") - - self.table_type = self.errorTitle[len("Error in "):] - self.i = 0 - - if adjustScrollWindow: - self.scrollBar = self.adjustScrollWindow.get_vscrollbar() - #this next line does not work on early versions of pygtk :( - self.scrollBar.connect('visibility-notify-event', self.scrollbar_visibility_change) - self.connect("size-request", self.size_adjustment) - self.connect("add", self.size_adjustment) - self.connect("remove", self.size_adjustment) - - # get scrollbar thickness from parent app scrollbar - very hackish, but what to do?? - self.bump = 16# self.parentApp.parentApp.image_scrolledwindow.get_hscrollbar().allocation.height - self.haveVerticalScrollbar = False - - # vbar is '1' if there is not vertical scroll bar - # if there is a vertical scroll bar, then it will have a the width of the bar - #self.vbar = self.adjustScrollWindow.get_vscrollbar().allocation.width - - self.getParentAppPrefs() - self.getPrefsFactory() - self.prefsFactory.setDownloadStartTime(datetime.datetime.now()) + if process_id in self.map_process_to_row: + treerowref = self.map_process_to_row[process_id] + path = treerowref.get_path() + iter = self.liststore.get_iter(path) + return iter + else: + return None + + def update_progress(self, process_id, percent_complete, progress_bar_text, bytes_downloaded): - try: - self.prefsFactory.checkPrefsForValidity() - - except (rn.PrefValueInvalidError, rn.PrefLengthError, - rn.PrefValueKeyComboError, rn.PrefKeyError), e: - - sys.stderr.write(self.errorTitle + "\n") - sys.stderr.write(_("Sorry,these preferences contain an error:\n")) - sys.stderr.write(self.prefsFactory.formatPreferencesForPrettyPrint() + "\n") - - # the preferences were invalid - # reset them to their default + iter = self._get_process_map(process_id) + 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 percent_complete or bytes_downloaded: + pass + #~ logger.info("Implement update overall progress") - self.prefList = self.prefsFactory.defaultPrefs - self.getPrefsFactory() - self.updateParentAppPrefs() - msg = "%s.\n" % e - msg += _("Resetting to default values." + "\n") - sys.stderr.write(msg) - - - misc.run_dialog(self.errorTitle, msg, - parentApp, - gtk.MESSAGE_ERROR) - - for row in self.prefsFactory.getWidgetsBasedOnPreferences(): - self.append(row) - - def updatePreferences(self): - prefList = [] - for row in self.pm_rows: - for col in range(self.pm_noColumns): - widget = row[col] - if widget: - name = widget.get_name() - if name == 'GtkComboBox': - value = widget.get_active_text() - elif name == 'GtkEntry': - value = widget.get_text() - else: - sys.stderr.write("Program error: Unknown preference widget!") - value = '' - else: - value = '' - prefList.append(value) +def create_cairo_image_surface(pil_image, image_width, image_height): + imgd = pil_image.tostring("raw","BGRA", 0, 1) + data = array.array('B',imgd) + stride = image_width * 4 + image = cairo.ImageSurface.create_for_data(data, cairo.FORMAT_ARGB32, + image_width, image_height, stride) + return image - self.prefList = prefList - self.updateParentAppPrefs() - self.prefsFactory.prefList = prefList - self.updateExample() - +class ThumbnailCellRenderer(gtk.CellRenderer): + __gproperties__ = { + "image": (gobject.TYPE_PYOBJECT, "Image", + "Image", gobject.PARAM_READWRITE), + + "filename": (gobject.TYPE_STRING, "Filename", + "Filename", '', gobject.PARAM_READWRITE), + + "status": (gtk.gdk.Pixbuf, "Status", + "Status", gobject.PARAM_READWRITE), + } - def scrollbar_visibility_change(self, widget, event): - if event.state == gdk.VISIBILITY_UNOBSCURED: - self.haveVerticalScrollbar = True - self.adjustScrollWindow.set_size_request(self.adjustScrollWindow.allocation.width + self.bump, -1) - - - def size_adjustment(self, widget, arg2): - """ - Adjust scrolledwindow width in preferences dialog to reflect width of image rename table + def __init__(self, checkbutton_height): + gtk.CellRenderer.__init__(self) + self.image = None - The algorithm is complicated by the need to take into account the presence of a vertical scrollbar, - which might be added as the user adds more rows + self.image_area_size = 100 + self.text_area_size = 30 + self.padding = 6 + self.checkbutton_height = checkbutton_height + self.icon_width = 20 - The pygtk code behaves inconsistently depending on the pygtk version - """ + def do_set_property(self, pspec, value): + setattr(self, pspec.name, value) + + def do_get_property(self, pspec): + return getattr(self, pspec.name) - if self.adjustScrollWindow: - self.haveVerticalScrollbar = self.scrollBar.allocation.width > 1 or self.haveVerticalScrollbar - if not self.haveVerticalScrollbar: - if self.allocation.width > self.adjustScrollWindow.allocation.width: - self.adjustScrollWindow.set_size_request(self.allocation.width, -1) - else: - if self.allocation.width > self.adjustScrollWindow.allocation.width - self.bump: - self.adjustScrollWindow.set_size_request(self.allocation.width + self.bump, -1) - self.bump = 0 - - def getParentAppPrefs(self): - self.prefList = self.parentApp.prefs.image_rename + def do_render(self, window, widget, background_area, cell_area, expose_area, flags): - - def getPrefsFactory(self): - self.prefsFactory = rn.ImageRenamePreferences(self.prefList, self, - sequences = sequences) + cairo_context = window.cairo_create() - def updateParentAppPrefs(self): - self.parentApp.prefs.image_rename = self.prefList + x = cell_area.x + y = cell_area.y + self.checkbutton_height - 8 + w = cell_area.width + h = cell_area.height - def updateExampleJobCode(self): - job_code = self.parentApp.prefs.getSampleJobCode() - if not job_code: - job_code = _('Job code') - self.prefsFactory.setJobCode(job_code) - def updateExample(self): - self.parentApp.updateImageRenameExample() - - def getDefaultRow(self): - return self.prefsFactory.getDefaultRow() - - def on_combobox_changed(self, widget, rowPosition): - - for col in range(self.pm_noColumns): - if self.pm_rows[rowPosition][col] == widget: - break - selection = [] - for i in range(col + 1): - # ensure it is a combo box we are getting the value from - w = self.pm_rows[rowPosition][i] - name = w.get_name() - if name == 'GtkComboBox': - selection.append(w.get_active_text()) - else: - selection.append(w.get_text()) - - for i in range(col + 1, self.pm_noColumns): - selection.append('') - - if col <> (self.pm_noColumns - 1): - widgets = self.prefsFactory.getWidgetsBasedOnUserSelection(selection) - - for i in range(col + 1, self.pm_noColumns): - oldWidget = self.pm_rows[rowPosition][i] - if oldWidget: - self.remove(oldWidget) - if oldWidget in self.pm_callbacks: - del self.pm_callbacks[oldWidget] - newWidget = widgets[i] - self.pm_rows[rowPosition][i] = newWidget - if newWidget: - self._createCallback(newWidget, rowPosition) - self.attach(newWidget, i, i+1, rowPosition, rowPosition + 1) - newWidget.show() - self.updatePreferences() - - - def on_entry_changed(self, widget, rowPosition): - self.updatePreferences() - - def on_rowAdded(self, rowPosition): - """ - Update preferences, as a row has been added - """ - self.updatePreferences() + #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() - # if this was the last row or 2nd to last row, and another has just been added, move vertical scrollbar down - if rowPosition in range(self.pm_noRows - 3, self.pm_noRows - 2): - adjustment = self.parentApp.rename_scrolledwindow.get_vadjustment() - adjustment.set_value(adjustment.upper) + #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() - - def on_rowDeleted(self, rowPosition): - """ - Update preferences, as a row has been deleted - """ - self.updatePreferences() - -class VideoRenameTable(ImageRenameTable): - def __init__(self, parentApp, adjustScollWindow): - self.errorTitle = _("Error in Video Rename preferences") - ImageRenameTable.__init__(self, parentApp, adjustScollWindow) - - def getParentAppPrefs(self): - self.prefList = self.parentApp.prefs.video_rename - - def getPrefsFactory(self): - self.prefsFactory = rn.VideoRenamePreferences(self.prefList, self, - sequences = sequences) + #image width and height + image_w = self.image.size[0] + image_h = self.image.size[1] - def updateParentAppPrefs(self): - self.parentApp.prefs.video_rename = self.prefList - - def updateExample(self): - self.parentApp.updateVideoRenameExample() + #center the image horizontally + #bottom align vertically + #top left and right corners for the image: + image_x = x + ((w - image_w) / 2) + image_y = y + self.image_area_size - image_h -class SubfolderTable(ImageRenameTable): - def __init__(self, parentApp, adjustScollWindow): - self.errorTitle = _("Error in Photo Download Subfolders preferences") - ImageRenameTable.__init__(self, parentApp, adjustScollWindow) + #convert PIL image to format suitable for cairo + image = create_cairo_image_surface(self.image, image_w, image_h) - def getParentAppPrefs(self): - self.prefList = self.parentApp.prefs.subfolder - - def getPrefsFactory(self): - self.prefsFactory = rn.SubfolderPreferences(self.prefList, self) + # draw a light grey border of 1px around the image + cairo_context.set_source_rgb(0.66, 0.66, 0.66) #light grey, #a9a9a9 + cairo_context.set_line_width(1) + cairo_context.rectangle(image_x-.5, image_y-.5, image_w+1, image_h+1) + cairo_context.stroke() - def updateParentAppPrefs(self): - self.parentApp.prefs.subfolder = self.prefList - - def updateExample(self): - self.parentApp.updatePhotoDownloadFolderExample() + # draw a thin border around each cell + # ouch - nasty hardcoding :( + #~ cairo_context.set_source_rgb(0.33, 0.33, 0.33) + #~ cairo_context.rectangle(x-6.5, y-9.5, w+14, h+31) + #~ cairo_context.stroke() + + #place the image + cairo_context.set_source_surface(image, image_x, image_y) + cairo_context.paint() + + #text + context = pangocairo.CairoContext(cairo_context) -class VideoSubfolderTable(ImageRenameTable): - def __init__(self, parentApp, adjustScollWindow): - self.errorTitle = _("Error in Video Download Subfolders preferences") - ImageRenameTable.__init__(self, parentApp, adjustScollWindow) + text_y = y + self.image_area_size + 10 + text_w = w - self.icon_width + text_x = x + self.icon_width + #~ context.rectangle(text_x, text_y, text_w, 15) + #~ context.clip() + + layout = context.create_layout() - def getParentAppPrefs(self): - self.prefList = self.parentApp.prefs.video_subfolder - - def getPrefsFactory(self): - self.prefsFactory = rn.VideoSubfolderPreferences(self.prefList, self) + width = text_w * pango.SCALE + layout.set_width(width) + + layout.set_alignment(pango.ALIGN_CENTER) + layout.set_ellipsize(pango.ELLIPSIZE_END) - def updateParentAppPrefs(self): - self.parentApp.prefs.video_subfolder = self.prefList + #font color and size + fg_color = pango.AttrForeground(65535, 65535, 65535, 0, -1) + font_size = pango.AttrSize(8192, 0, -1) # 8 * 1024 = 8192 + font_family = pango.AttrFamily('sans', 0, -1) + attr = pango.AttrList() + attr.insert(fg_color) + attr.insert(font_size) + attr.insert(font_family) + layout.set_attributes(attr) - def updateExample(self): - self.parentApp.updateVideoDownloadFolderExample() + layout.set_text(self.filename) -class PreferencesDialog(gnomeglade.Component): - def __init__(self, parentApp): - gnomeglade.Component.__init__(self, - paths.share_dir(config.GLADE_FILE), - "preferencesdialog") - - self.widget.set_transient_for(parentApp.widget) - self.prefs = parentApp.prefs + context.move_to(text_x, text_y) + context.show_layout(layout) + + #status + cairo_context.set_source_pixbuf(self.status, x, y + self.image_area_size + 10) + cairo_context.paint() - parentApp.preferencesDialogDisplayed = True + def do_get_size(self, widget, cell_area): + #~ return (0, 0, self.image_area_size, self.image_area_size + self.checkbutton_height + 10) + return (0, 0, self.image_area_size, self.image_area_size + self.text_area_size - self.checkbutton_height + 4) - self.parentApp = parentApp - self._setupTabSelector() +gobject.type_register(ThumbnailCellRenderer) + + +class ThumbnailDisplay(gtk.IconView): + def __init__(self, parent_app): + gtk.IconView.__init__(self) + self.rapid_app = parent_app - self._setupControlSpacing() + self.batch_size = 10 - if DOWNLOAD_VIDEO: - self.file_types = _("photos and videos") - else: - self.file_types = _("photos") - - # get example photo and video data - try: - w = workers.firstWorkerReadyToDownload() - mediaFile = w.firstImage() - self.sampleImageName = mediaFile.name - # assume the metadata is already read - self.sampleImage = mediaFile.metadata - except: - self.sampleImage = metadata.DummyMetaData() - self.sampleImageName = 'IMG_0524.CR2' - - try: - mediaFile = w.firstVideo() - self.sampleVideoName = mediaFile.name - self.sampleVideo = mediaFile.metadata - self.videoFallBackDate = mediaFile.modificationTime - except: - self.sampleVideo = videometadata.DummyMetaData() - self.sampleVideoName = 'MVI_1379.MOV' - self.videoFallBackDate = datetime.datetime.now() - + self.thumbnail_manager = ThumbnailManager(self.thumbnail_results, self.batch_size) + self.preview_manager = PreviewManager(self.preview_results) - # setup tabs - self._setupPhotoDownloadFolderTab() - self._setupImageRenameTab() - self._setupVideoDownloadFolderTab() - self._setupVideoRenameTab() - self._setupRenameOptionsTab() - self._setupJobCodeTab() - self._setupDeviceTab() - self._setupBackupTab() - self._setupAutomationTab() - self._setupErrorTab() - - if not DOWNLOAD_VIDEO: - self.disableVideoControls() - - self.widget.realize() - - #set the width of the left column for selecting values - #note: this must be called after self.widget.realize(), or else the width calculation will fail - width_of_widest_sel_row = self.treeview.get_background_area(1, self.treeview_column)[2] - self.scrolled_window.set_size_request(width_of_widest_sel_row + 2, -1) - - #set the minimum width of the scolled window holding the photo rename table - if self.rename_scrolledwindow.get_vscrollbar(): - extra = self.rename_scrolledwindow.get_vscrollbar().allocation.width + 10 - else: - extra = 10 - self.rename_scrolledwindow.set_size_request(self.rename_table.allocation.width + extra, -1) - - self.widget.show() - - def on_preferencesdialog_destroy(self, widget): - """ Delete variables from memory that cause a file descriptor to be created on a mounted media""" - del self.sampleImage, self.rename_table.prefsFactory, self.subfolder_table.prefsFactory - - def _setupTabSelector(self): - self.notebook.set_show_tabs(0) - self.model = gtk.ListStore(type("")) - column = gtk.TreeViewColumn() - rentext = gtk.CellRendererText() - column.pack_start(rentext, expand=0) - column.set_attributes(rentext, text=0) - self.treeview_column = column - self.treeview.append_column(column) - self.treeview.props.model = self.model - for c in self.notebook.get_children(): - label = self.notebook.get_tab_label(c).get_text() - if not label.startswith("_"): - self.model.append( (label,) ) - - - # select the first value in the list store - self.treeview.set_cursor(0,column) - - def on_download_folder_filechooser_button_selection_changed(self, widget): - self.prefs.download_folder = widget.get_current_folder() - self.updatePhotoDownloadFolderExample() + self.treerow_index = {} + self.process_index = {} - def on_video_download_folder_filechooser_button_selection_changed(self, widget): - self.prefs.video_download_folder = widget.get_current_folder() - self.updateVideoDownloadFolderExample() - - def on_backup_folder_filechooser_button_selection_changed(self, widget): - self.prefs.backup_location = widget.get_current_folder() - self.updateBackupExample() + self.rpd_files = {} - def on_device_location_filechooser_button_selection_changed(self, widget): - self.prefs.device_location = widget.get_current_folder() + self.total_files = 0 + self.thumbnails_generated = 0 - def _setupControlSpacing(self): - """ - set spacing of some but not all controls - """ + self.thumbnails = {} + self.previews = {} + self.previews_being_fetched = set() - self._setupTableSpacing(self.download_folder_table) - self._setupTableSpacing(self.video_download_folder_table) - self.download_folder_table.set_row_spacing(2, - hd.VERTICAL_CONTROL_SPACE) - self.video_download_folder_table.set_row_spacing(2, - hd.VERTICAL_CONTROL_SPACE) - self._setupTableSpacing(self.rename_example_table) - self._setupTableSpacing(self.video_rename_example_table) - self.devices_table.set_col_spacing(0, hd.NESTED_CONTROLS_SPACE) - - self._setupTableSpacing(self.backup_table) - self.backup_table.set_col_spacing(1, hd.NESTED_CONTROLS_SPACE) - self.backup_table.set_col_spacing(2, hd.CONTROL_LABEL_SPACE) - self._setupTableSpacing(self.compatibility_table) - self.compatibility_table.set_row_spacing(0, - hd.VERTICAL_CONTROL_LABEL_SPACE) - self._setupTableSpacing(self.error_table) - - - def _setupTableSpacing(self, table): - table.set_col_spacing(0, hd.NESTED_CONTROLS_SPACE) - table.set_col_spacing(1, hd.CONTROL_LABEL_SPACE) - - def _setupSubfolderTable(self): - self.subfolder_table = SubfolderTable(self, None) - self.subfolder_vbox.pack_start(self.subfolder_table) - self.subfolder_table.show_all() - - def _setupVideoSubfolderTable(self): - self.video_subfolder_table = VideoSubfolderTable(self, None) - self.video_subfolder_vbox.pack_start(self.video_subfolder_table) - self.video_subfolder_table.show_all() - - def _setupPhotoDownloadFolderTab(self): - self.download_folder_filechooser_button = gtk.FileChooserButton( - _("Select a folder to download photos to")) - self.download_folder_filechooser_button.set_current_folder( - self.prefs.download_folder) - self.download_folder_filechooser_button.set_action( - gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) - self.download_folder_filechooser_button.connect("selection-changed", - self.on_download_folder_filechooser_button_selection_changed) - - self.download_folder_table.attach( - self.download_folder_filechooser_button, - 2, 3, 2, 3, yoptions = gtk.SHRINK) - self.download_folder_filechooser_button.show() - - self._setupSubfolderTable() - self.updatePhotoDownloadFolderExample() - - def _setupVideoDownloadFolderTab(self): - self.video_download_folder_filechooser_button = gtk.FileChooserButton( - _("Select a folder to download videos to")) - self.video_download_folder_filechooser_button.set_current_folder( - self.prefs.video_download_folder) - self.video_download_folder_filechooser_button.set_action( - gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) - self.video_download_folder_filechooser_button.connect("selection-changed", - self.on_video_download_folder_filechooser_button_selection_changed) - - self.video_download_folder_table.attach( - self.video_download_folder_filechooser_button, - 2, 3, 2, 3, yoptions = gtk.SHRINK) - self.video_download_folder_filechooser_button.show() - self._setupVideoSubfolderTable() - self.updateVideoDownloadFolderExample() - - def _setupImageRenameTab(self): - - self.rename_table = ImageRenameTable(self, self.rename_scrolledwindow) - self.rename_table_vbox.pack_start(self.rename_table) - self.rename_table.show_all() - self.original_name_label.set_markup("%s" % self.sampleImageName) - self.updateImageRenameExample() + self.stock_photo_thumbnails = tn.PhotoIcons() + self.stock_video_thumbnails = tn.VideoIcons() + + self.SELECTED_COL = 1 + self.UNIQUE_ID_COL = 2 + self.TIMESTAMP_COL = 4 + self.FILETYPE_COL = 5 + self.CHECKBUTTON_VISIBLE_COL = 6 + self.DOWNLOAD_STATUS_COL = 7 + self.STATUS_ICON_COL = 8 - def _setupVideoRenameTab(self): + self.liststore = gtk.ListStore( + gobject.TYPE_PYOBJECT, # 0 PIL thumbnail + gobject.TYPE_BOOLEAN, # 1 selected or not + str, # 2 unique id + str, # 3 file name + int, # 4 timestamp for sorting, converted float + int, # 5 file type i.e. photo or video + gobject.TYPE_BOOLEAN, # 6 visibility of checkbutton + int, # 7 status of download + gtk.gdk.Pixbuf, # 8 status icon + ) - self.video_rename_table = VideoRenameTable(self, self.video_rename_scrolledwindow) - self.video_rename_table_vbox.pack_start(self.video_rename_table) - self.video_rename_table.show_all() - self.video_original_name_label.set_markup("%s" % self.sampleVideoName) - self.updateVideoRenameExample() - - def _setupRenameOptionsTab(self): - - # sequence numbers - self.downloads_today_entry = ValidatedEntry.ValidatedEntry(ValidatedEntry.bounded(ValidatedEntry.v_int, int, 0)) - self.stored_number_entry = ValidatedEntry.ValidatedEntry(ValidatedEntry.bounded(ValidatedEntry.v_int, int, 1)) - self.downloads_today_entry.connect('changed', self.on_downloads_today_entry_changed) - self.stored_number_entry.connect('changed', self.on_stored_number_entry_changed) - v = self.prefs.getAndMaybeResetDownloadsToday() - self.downloads_today_entry.set_text(str(v)) - # make the displayed value of stored sequence no 1 more than actual value - # so as not to confuse the user - self.stored_number_entry.set_text(str(self.prefs.stored_sequence_no+1)) - self.sequence_vbox.pack_start(self.downloads_today_entry, expand=True, fill=True) - self.sequence_vbox.pack_start(self.stored_number_entry, expand=False) - self.downloads_today_entry.show() - self.stored_number_entry.show() - hour, minute = self.prefs.getDayStart() - self.hour_spinbutton.set_value(float(hour)) - self.minute_spinbutton.set_value(float(minute)) - - self.synchronize_raw_jpg_checkbutton.set_active( - self.prefs.synchronize_raw_jpg) - - #compatibility - self.strip_characters_checkbutton.set_active( - self.prefs.strip_characters) - - def _setupJobCodeTab(self): - self.job_code_liststore = gtk.ListStore(str) - column = gtk.TreeViewColumn() - rentext = gtk.CellRendererText() - rentext.connect('edited', self.on_job_code_edited) - rentext .set_property('editable', True) - - column.pack_start(rentext, expand=0) - column.set_attributes(rentext, text=0) - self.job_code_treeview_column = column - self.job_code_treeview.append_column(column) - self.job_code_treeview.props.model = self.job_code_liststore - for code in self.prefs.job_codes: - self.job_code_liststore.append((code, )) - - # set multiple selections - self.job_code_treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE) - - self.remove_all_job_code_button.set_image(gtk.image_new_from_stock( - gtk.STOCK_CLEAR, - gtk.ICON_SIZE_BUTTON)) - def _setupDeviceTab(self): - - self.device_location_filechooser_button = gtk.FileChooserButton( - _("Select a folder containing %(file_types)s") % {'file_types':self.file_types}) - self.device_location_filechooser_button.set_current_folder( - self.prefs.device_location) - self.device_location_filechooser_button.set_action( - gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) - - self.device_location_filechooser_button.connect("selection-changed", - self.on_device_location_filechooser_button_selection_changed) - - self.devices2_table.attach(self.device_location_filechooser_button, - 1, 2, 1, 2, xoptions = gtk.EXPAND|gtk.FILL, yoptions = gtk.SHRINK) - self.device_location_filechooser_button.show() - self.autodetect_device_checkbutton.set_active( - self.prefs.device_autodetection) - self.autodetect_psd_checkbutton.set_active( - self.prefs.device_autodetection_psd) - - self.updateDeviceControls() - - - def _setupBackupTab(self): - self.backup_folder_filechooser_button = gtk.FileChooserButton( - _("Select a folder in which to backup %(file_types)s") % {'file_types':self.file_types}) - self.backup_folder_filechooser_button.set_current_folder( - self.prefs.backup_location) - self.backup_folder_filechooser_button.set_action( - gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) - self.backup_folder_filechooser_button.connect("selection-changed", - self.on_backup_folder_filechooser_button_selection_changed) - self.backup_table.attach(self.backup_folder_filechooser_button, - 3, 4, 8, 9, yoptions = gtk.SHRINK) - self.backup_folder_filechooser_button.show() - self.backup_identifier_entry.set_text(self.prefs.backup_identifier) - self.video_backup_identifier_entry.set_text(self.prefs.video_backup_identifier) - - #setup controls for manipulating sensitivity - self._backupControls0 = [self.auto_detect_backup_checkbutton] - self._backupControls1 = [self.backup_identifier_explanation_label, - self.backup_identifier_label, - self.backup_identifier_entry, - self.example_backup_path_label, - self.backup_example_label,] - self._backupControls2 = [self.backup_location_label, - self.backup_folder_filechooser_button, - self.backup_location_explanation_label] - self._backupControls = self._backupControls0 + self._backupControls1 + \ - self._backupControls2 - - self._backupVideoControls = [self.video_backup_identifier_label, - self.video_backup_identifier_entry] - - #assign values to checkbuttons only when other controls - #have been setup, because their toggle signal is activated - #when a value is assigned - - self.backup_checkbutton.set_active(self.prefs.backup_images) - self.auto_detect_backup_checkbutton.set_active( - self.prefs.backup_device_autodetection) - self.updateBackupControls() - self.updateBackupExample() - - def _setupAutomationTab(self): - self.auto_startup_checkbutton.set_active( - self.prefs.auto_download_at_startup) - self.auto_insertion_checkbutton.set_active( - self.prefs.auto_download_upon_device_insertion) - self.auto_unmount_checkbutton.set_active( - self.prefs.auto_unmount) - self.auto_exit_checkbutton.set_active( - self.prefs.auto_exit) - self.auto_delete_checkbutton.set_active( - self.prefs.auto_delete) - - - def _setupErrorTab(self): - if self.prefs.download_conflict_resolution == config.SKIP_DOWNLOAD: - self.skip_download_radiobutton.set_active(True) - else: - self.add_identifier_radiobutton.set_active(True) - - if self.prefs.backup_duplicate_overwrite: - self.backup_duplicate_overwrite_radiobutton.set_active(True) - else: - self.backup_duplicate_skip_radiobutton.set_active(True) - - def updateExampleFileName(self, display_table, rename_table, sample, sampleName, example_label, fallback_date = None): - problem = pn.Problem() - if hasattr(self, display_table): - rename_table.updateExampleJobCode() - rename_table.prefsFactory.initializeProblem(problem) - name = rename_table.prefsFactory.generateNameUsingPreferences( - sample, sampleName, - self.prefs.strip_characters, sequencesPreliminary=False, fallback_date=fallback_date) - else: - name = '' - - # since this is markup, escape it - text = "%s" % common.escape(name) + self.clear() + self.set_model(self.liststore) - if problem.has_problem(): - text += "\n" - # Translators: please do not modify or leave out html formatting tags like and . These are used to format the text the users sees - text += _("Warning: There is insufficient metadata to fully generate the name. Please use other renaming options.") - - example_label.set_markup(text) - - def updateImageRenameExample(self): - """ - Displays example image name to the user - """ - self.updateExampleFileName('rename_table', self.rename_table, self.sampleImage, self.sampleImageName, self.new_name_label) + checkbutton = gtk.CellRendererToggle() + checkbutton.set_radio(False) + checkbutton.props.activatable = True + checkbutton.props.xalign = 0.0 + checkbutton.connect('toggled', self.on_checkbutton_toggled) + self.pack_end(checkbutton, expand=False) + self.add_attribute(checkbutton, "active", 1) + self.add_attribute(checkbutton, "visible", 6) + + checkbutton_size = checkbutton.get_size(self, None) + checkbutton_height = checkbutton_size[3] + checkbutton_width = checkbutton_size[2] + + #~ status_icon = gtk.CellRendererPixbuf() + #~ self.pack_start(status_icon, expand=False) + #~ self.add_attribute(status_icon, "pixbuf", self.STATUS_ICON_COL) + + image = ThumbnailCellRenderer(checkbutton_height) + self.pack_start(image, expand=True) + self.add_attribute(image, "image", 0) + self.add_attribute(image, "filename", 3) + self.add_attribute(image, "status", 8) - def updateVideoRenameExample(self): - """ - Displays example video name to the user - """ - self.updateExampleFileName('video_rename_table', self.video_rename_table, self.sampleVideo, self.sampleVideoName, self.video_new_name_label, self.videoFallBackDate) - - def updateDownloadFolderExample(self, display_table, subfolder_table, download_folder, sample, sampleName, example_download_path_label, subfolder_warning_label, fallback_date = None): - """ - Displays example subfolder name(s) to the user - """ + #set the background color to a darkish grey + self.modify_base(gtk.STATE_NORMAL, gtk.gdk.Color('#444444')) - problem = pn.Problem() - if hasattr(self, display_table): - subfolder_table.updateExampleJobCode() - subfolder_table.prefsFactory.initializeProblem(problem) - path = subfolder_table.prefsFactory.generateNameUsingPreferences( - sample, sampleName, - self.prefs.strip_characters, fallback_date = fallback_date) - else: - path = '' + self.set_spacing(0) + #~ self.set_column_spacing(0) + self.set_row_spacing(5) + #~ self.set_row_spacing(0) + self.set_margin(25) - text = os.path.join(download_folder, path) - # since this is markup, escape it - path = common.escape(text) - if problem.has_problem(): - warning = _("Warning: There is insufficient metadata to fully generate subfolders. Please use other subfolder naming options." ) - else: - warning = "" - # Translators: you should not modify or leave out the %s. This is a code used by the programming language python to insert a value that thes user will see - example_download_path_label.set_markup(_("Example: %s") % text) - subfolder_warning_label.set_markup(warning) - - def updatePhotoDownloadFolderExample(self): - if hasattr(self, 'subfolder_table'): - self.updateDownloadFolderExample('subfolder_table', self.subfolder_table, self.prefs.download_folder, self.sampleImage, self.sampleImageName, self.example_photo_download_path_label, self.photo_subfolder_warning_label) - - def updateVideoDownloadFolderExample(self): - if hasattr(self, 'video_subfolder_table'): - self.updateDownloadFolderExample('video_subfolder_table', self.video_subfolder_table, self.prefs.video_download_folder, self.sampleVideo, self.sampleVideoName, self.example_video_download_path_label, self.video_subfolder_warning_label, self.videoFallBackDate) - - def on_hour_spinbutton_value_changed(self, spinbutton): - hour = spinbutton.get_value_as_int() - minute = self.minute_spinbutton.get_value_as_int() - self.prefs.setDayStart(hour, minute) - self.on_downloads_today_entry_changed(self.downloads_today_entry) - - def on_minute_spinbutton_value_changed(self, spinbutton): - hour = self.hour_spinbutton.get_value_as_int() - minute = spinbutton.get_value_as_int() - self.prefs.setDayStart(hour, minute) - self.on_downloads_today_entry_changed(self.downloads_today_entry) - - def on_downloads_today_entry_changed(self, entry): - # do not update value if a download is occurring - it will mess it up! - if workers.noDownloadingWorkers() <> 0: - cmd_line(_("Downloads today value not updated, as a download is currently occurring")) - else: - v = entry.get_text() - try: - v = int(v) - except: - v = 0 - if v < 0: - v = 0 - self.prefs.resetDownloadsToday(v) - sequences.setDownloadsToday(v) - self.updateImageRenameExample() - - def on_stored_number_entry_changed(self, entry): - # do not update value if a download is occurring - it will mess it up! - if workers.noDownloadingWorkers() <> 0: - cmd_line(_("Stored number value not updated, as a download is currently occurring")) - else: - v = entry.get_text() - try: - # the displayed value of stored sequence no 1 more than actual value - # so as not to confuse the user - v = int(v) - 1 - except: - v = 0 - if v < 0: - v = 0 - self.prefs.stored_sequence_no = v - sequences.setStoredSequenceNo(v) - self.updateImageRenameExample() - - def _updateSubfolderPrefOnError(self, newPrefList): - self.prefs.subfolder = newPrefList - - def _updateVideoSubfolderPrefOnError(self, newPrefList): - self.prefs.video_subfolder = newPrefList + self._setup_icons() - - def checkSubfolderValuesValidOnExit(self, usersPrefList, updatePrefFunction, filetype, defaultPrefList): - """ - Checks that the user has not entered in any inappropriate values + self.show_all() - If they have, filters out bad values and warns the user - """ - filtered, prefList = rn.filterSubfolderPreferences(usersPrefList) - if filtered: - cmd_line(_("The %(filetype)s subfolder preferences had some unnecessary values removed.") % {'filetype': filetype}) - if prefList: - updatePrefFunction(prefList) - else: - #Preferences list is now empty - msg = _("The %(filetype)s subfolder preferences entered are invalid and cannot be used.\nThey will be reset to their default values.") % {'filetype': filetype} - sys.stderr.write(msg + "\n") - misc.run_dialog(PROGRAM_NAME, msg) - updatePrefFunction(self.prefs.get_default(defaultPrefList)) - - def on_response(self, dialog, arg): - if arg == gtk.RESPONSE_HELP: - webbrowser.open("http://www.damonlynch.net/rapid/documentation") - else: - # arg==gtk.RESPONSE_CLOSE, or the user hit the 'x' to close the window - self.prefs.backup_identifier = self.backup_identifier_entry.get_property("text") - self.prefs.video_backup_identifier = self.video_backup_identifier_entry.get_property("text") - - #check subfolder preferences for bad values - self.checkSubfolderValuesValidOnExit(self.prefs.subfolder, self._updateSubfolderPrefOnError, _("photo"), "subfolder") - self.checkSubfolderValuesValidOnExit(self.prefs.video_subfolder, self._updateVideoSubfolderPrefOnError, _("video"), "video_subfolder") - - self.widget.destroy() - self.parentApp.preferencesDialogDisplayed = False - self.parentApp.postPreferenceChange() - - - - - def on_add_job_code_button_clicked(self, button): - j = JobCodeDialog(self.widget, self.prefs.job_codes, None, self.add_job_code, False, True, True) - - - def add_job_code(self, dialog, userChoseCode, job_code, autoStart, downloadSelected): - dialog.destroy() - if userChoseCode: - if job_code and job_code not in self.prefs.job_codes: - self.job_code_liststore.prepend((job_code, )) - self.update_job_codes() - selection = self.job_code_treeview.get_selection() - selection.unselect_all() - selection.select_path((0, )) - #scroll to the top - adjustment = self.job_code_scrolledwindow.get_vadjustment() - adjustment.set_value(adjustment.lower) - - def on_remove_job_code_button_clicked(self, button): - """ remove selected job codes (can be multiple selection)""" - selection = self.job_code_treeview.get_selection() - model, selected = selection.get_selected_rows() - iters = [model.get_iter(path) for path in selected] - # only delete if a jobe code is selected - if iters: - no = len(iters) - path = None - for i in range(0, no): - iter = iters[i] - if i == no - 1: - path = model.get_path(iter) - model.remove(iter) - - # now that we removed the selection, play nice with - # the user and select the next item - selection.select_path(path) - - # if there was no selection that meant the user - # removed the last entry, so we try to select the - # last item - if not selection.path_is_selected(path): - row = path[0]-1 - # test case for empty lists - if row >= 0: - selection.select_path((row,)) - - self.update_job_codes() - self.updateImageRenameExample() - self.updateVideoRenameExample() - self.updatePhotoDownloadFolderExample() - self.updateVideoDownloadFolderExample() - - def on_remove_all_job_code_button_clicked(self, button): - j = RemoveAllJobCodeDialog(self.widget, self.remove_all_job_code) - - def remove_all_job_code(self, dialog, userSelected): - dialog.destroy() - if userSelected: - self.job_code_liststore.clear() - self.update_job_codes() - self.updateImageRenameExample() - self.updateVideoRenameExample() - self.updatePhotoDownloadFolderExample() - self.updateVideoDownloadFolderExample() - - def on_job_code_edited(self, widget, path, new_text): - iter = self.job_code_liststore.get_iter(path) - self.job_code_liststore.set_value(iter, 0, new_text) - self.update_job_codes() - self.updateImageRenameExample() - self.updateVideoRenameExample() - self.updatePhotoDownloadFolderExample() - self.updateVideoDownloadFolderExample() - - def update_job_codes(self): - """ update preferences with list of job codes""" - job_codes = [] - for row in self.job_code_liststore: - job_codes.append(row[0]) - self.prefs.job_codes = job_codes - - def on_auto_startup_checkbutton_toggled(self, checkbutton): - self.prefs.auto_download_at_startup = checkbutton.get_active() - - def on_auto_insertion_checkbutton_toggled(self, checkbutton): - self.prefs.auto_download_upon_device_insertion = checkbutton.get_active() - - def on_auto_unmount_checkbutton_toggled(self, checkbutton): - self.prefs.auto_unmount = checkbutton.get_active() - - - def on_auto_delete_checkbutton_toggled(self, checkbutton): - self.prefs.auto_delete = checkbutton.get_active() - - def on_auto_exit_checkbutton_toggled(self, checkbutton): - self.prefs.auto_exit = checkbutton.get_active() - - def on_autodetect_device_checkbutton_toggled(self, checkbutton): - self.prefs.device_autodetection = checkbutton.get_active() - self.updateDeviceControls() - - def on_autodetect_psd_checkbutton_toggled(self, checkbutton): - self.prefs.device_autodetection_psd = checkbutton.get_active() - - def on_backup_duplicate_overwrite_radiobutton_toggled(self, widget): - self.prefs.backup_duplicate_overwrite = widget.get_active() - - def on_backup_duplicate_skip_radiobutton_toggled(self, widget): - self.prefs.backup_duplicate_overwrite = not widget.get_active() - - def on_treeview_cursor_changed(self, tree): - path, column = tree.get_cursor() - self.notebook.set_current_page(path[0]) - - def on_synchronize_raw_jpg_checkbutton_toggled(self, check_button): - self.prefs.synchronize_raw_jpg = check_button.get_active() - - def on_strip_characters_checkbutton_toggled(self, check_button): - self.prefs.strip_characters = check_button.get_active() - self.updateImageRenameExample() - self.updatePhotoDownloadFolderExample() - self.updateVideoDownloadFolderExample() - - def on_add_identifier_radiobutton_toggled(self, widget): - if widget.get_active(): - self.prefs.download_conflict_resolution = config.ADD_UNIQUE_IDENTIFIER - else: - self.prefs.download_conflict_resolution = config.SKIP_DOWNLOAD - - - def updateDeviceControls(self): - """ - Sets sensitivity of image device controls - """ - controls = [self.device_location_explanation_label, - self.device_location_label, - self.device_location_filechooser_button] - - if self.prefs.device_autodetection: - for c in controls: - c.set_sensitive(False) - self.autodetect_psd_checkbutton.set_sensitive(True) - self.autodetect_image_devices_label.set_sensitive(True) - else: - for c in controls: - c.set_sensitive(True) - self.autodetect_psd_checkbutton.set_sensitive(False) - self.autodetect_image_devices_label.set_sensitive(False) - - def updateBackupControls(self): - """ - Sets sensitivity of backup related widgets - """ - - if not self.backup_checkbutton.get_active(): - for c in self._backupControls + self._backupVideoControls: - c.set_sensitive(False) - - else: - for c in self._backupControls0: - c.set_sensitive(True) - self.updateBackupControlsAuto() - - def updateBackupControlsAuto(self): - """ - Sets sensitivity of subset of backup related widgets - """ - - if self.auto_detect_backup_checkbutton.get_active(): - for c in self._backupControls1: - c.set_sensitive(True) - for c in self._backupControls2: - c.set_sensitive(False) - for c in self._backupVideoControls: - c.set_sensitive(False) - if DOWNLOAD_VIDEO: - for c in self._backupVideoControls: - c.set_sensitive(True) - else: - for c in self._backupControls1: - c.set_sensitive(False) - for c in self._backupControls2: - c.set_sensitive(True) - if DOWNLOAD_VIDEO: - for c in self._backupVideoControls: - c.set_sensitive(False) - - def disableVideoControls(self): - """ - Disables video preferences if video downloading is disabled - (probably because the appropriate libraries to enable - video metadata extraction are not installed) - """ - controls = [self.example_video_filename_label, - self.original_video_filename_label, - self.new_video_filename_label, - self.video_new_name_label, - self.video_original_name_label, - self.video_rename_scrolledwindow, - self.video_folders_hbox, - self.video_backup_identifier_label, - self.video_backup_identifier_entry - ] - for c in controls: - c.set_sensitive(False) - - self.videos_cannot_be_downloaded_label.show() - self.folder_videos_cannot_be_downloaded_label.show() - self.folder_videos_cannot_be_downloaded_hbox.show() - - def on_auto_detect_backup_checkbutton_toggled(self, widget): - self.prefs.backup_device_autodetection = widget.get_active() - self.updateBackupControlsAuto() - - def on_backup_checkbutton_toggled(self, widget): - self.prefs.backup_images = self.backup_checkbutton.get_active() - self.updateBackupControls() - - def on_backup_identifier_entry_changed(self, widget): - self.updateBackupExample() - - def on_video_backup_identifier_entry_changed(self, widget): - self.updateBackupExample() - - def on_backup_scan_folder_on_entry_changed(self, widget): - self.updateBackupExample() - - def updateBackupExample(self): - # Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. - drive1 = os.path.join(config.MEDIA_LOCATION, _("externaldrive1")) - # Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. - drive2 = os.path.join(config.MEDIA_LOCATION, _("externaldrive2")) - - path = os.path.join(drive1, self.backup_identifier_entry.get_text()) - path2 = os.path.join(drive2, self.backup_identifier_entry.get_text()) - path3 = os.path.join(drive2, self.video_backup_identifier_entry.get_text()) - path = common.escape(path) - path2 = common.escape(path2) - path3 = common.escape(path3) - if DOWNLOAD_VIDEO: - example = "%s\n%s\n%s" % (path, path2, path3) - else: - example = "%s\n%s" % (path, path2) - self.example_backup_path_label.set_markup(example) - - -def file_types_by_number(noImages, noVideos): - """ - returns a string to be displayed to the user that can be used - to show if a value refers to photos or videos or both, or just one - of each - """ - if (noVideos > 0) and (noImages > 0): - v = _('photos and videos') - elif (noVideos == 0) and (noImages == 0): - v = _('photos or videos') - elif noVideos > 0: - if noVideos > 1: - v = _('videos') - else: - v = _('video') - else: - if noImages > 1: - v = _('photos') - else: - v = _('photo') - return v - -def date_time_human_readable(date, with_line_break=True): - if with_line_break: - return _("%(date)s\n%(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")} - else: - return _("%(date)s %(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")} - -def time_subseconds_human_readable(date, subseconds): - return _("%(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \ - {'hour':date.strftime("%H"), - 'minute':date.strftime("%M"), - 'second':date.strftime("%S"), - 'subsecond': subseconds} - -def date_time_subseconds_human_readable(date, subseconds): - return _("%(date)s %(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \ - {'date':date.strftime("%x"), - 'hour':date.strftime("%H"), - 'minute':date.strftime("%M"), - 'second':date.strftime("%S"), - 'subsecond': subseconds} - -def generateSubfolderAndName(mediaFile, problem, subfolderPrefsFactory, - renamePrefsFactory, - nameUsesJobCode, subfolderUsesJobCode, - strip_characters, fallback_date): - - subfolderPrefsFactory.initializeProblem(problem) - mediaFile.sampleSubfolder = subfolderPrefsFactory.generateNameUsingPreferences( - mediaFile.metadata, mediaFile.name, - strip_characters, - fallback_date = fallback_date) - - mediaFile.samplePath = os.path.join(mediaFile.downloadFolder, mediaFile.sampleSubfolder) - - renamePrefsFactory.initializeProblem(problem) - mediaFile.sampleName = renamePrefsFactory.generateNameUsingPreferences( - mediaFile.metadata, mediaFile.name, strip_characters, - sequencesPreliminary=False, - fallback_date = fallback_date) - - if not (mediaFile.sampleName or nameUsesJobCode) or not (mediaFile.sampleSubfolder or subfolderUsesJobCode): - if not (mediaFile.sampleName or nameUsesJobCode) and not (mediaFile.sampleSubfolder or subfolderUsesJobCode): - area = _("subfolder and filename") - elif not (mediaFile.sampleName or nameUsesJobCode): - area = _("filename") - else: - area = _("subfolder") - problem.add_problem(None, pn.ERROR_IN_NAME_GENERATION, {'filetype': mediaFile.displayNameCap, 'area': area}) - problem.add_extra_detail(pn.NO_DATA_TO_NAME, {'filetype': area}) - mediaFile.problem = problem - mediaFile.status = STATUS_CANNOT_DOWNLOAD - elif problem.has_problem(): - mediaFile.problem = problem - mediaFile.status = STATUS_WARNING - else: - mediaFile.status = STATUS_NOT_DOWNLOADED - -def getGenericPhotoImage(): - return gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/photo.png')) - -def getGenericVideoImage(): - return gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/video.png')) - -class NeedAJobCode(): - """ - Convenience class to check whether a job code is missing for a given - file type (photo or video) - """ - def __init__(self, prefs): - self.imageRenameUsesJobCode = rn.usesJobCode(prefs.image_rename) - self.imageSubfolderUsesJobCode = rn.usesJobCode(prefs.subfolder) - self.videoRenameUsesJobCode = rn.usesJobCode(prefs.video_rename) - self.videoSubfolderUsesJobCode = rn.usesJobCode(prefs.video_subfolder) - - def needAJobCode(self, job_code, is_image): - if is_image: - return not job_code and (self.imageRenameUsesJobCode or self.imageSubfolderUsesJobCode) - else: - return not job_code and (self.videoRenameUsesJobCode or self.videoSubfolderUsesJobCode) - - -class CopyPhotos(Thread): - """Copies photos from source to destination, backing up if needed""" - def __init__(self, thread_id, parentApp, fileRenameLock, fileSequenceLock, - statsLock, downloadedFilesLock, - downloadStats, autoStart = False, cardMedia = None): - self.parentApp = parentApp - self.thread_id = thread_id - self.ctrl = True - self.running = False - self.manuallyDisabled = False - # enable the capacity to block oneself with a lock - # the lock will be first set when the thread begins - # it will then be locked when the thread needs to be paused - # releasing it will cause the code to restart from where it - # left off - self.lock = Lock() - - self.fileRenameLock = fileRenameLock - self.fileSequenceLock = fileSequenceLock - self.statsLock = statsLock - self.downloadedFilesLock = downloadedFilesLock - - self.downloadStats = downloadStats - - self.hasStarted = False - self.doNotStart = False - self.waitingForJobCode = False - - self.autoStart = autoStart - self.cardMedia = cardMedia - - self.initializeDisplay(thread_id, self.cardMedia) - - self.scanComplete = self.downloadStarted = self.downloadComplete = False - - # Need to account for situations where the user adjusts their preferences when the program is scanning - # Here the sample filenames and paths will be out of date, and they will need to be updated - # This flag indicates whether that is the case or not - self.scanResultsStale = False # name and subfolder - self.scanResultsStaleDownloadFolder = False #download folder only - - self.noErrors = self.noWarnings = 0 - self.videoTempWorkingDir = self.photoTempWorkingDir = '' - - if DOWNLOAD_VIDEO: - self.types_searched_for = _('photos or videos') - else: - self.types_searched_for = _('photos') - - Thread.__init__(self) - - - def initializeDisplay(self, thread_id, cardMedia = None): - - if self.cardMedia: - media_collection_treeview.addCard(thread_id, self.cardMedia.prettyName(), - '', progress=0.0, - # This refers to when a device like a hard drive is having its contents scanned, - # looking for photos or videos. It is visible initially in the progress bar for each device - # (which normally holds "x photos and videos"). - # It maybe displayed only briefly if the contents of the device being scanned is small. - progressBarText=_('scanning...')) - - def firstImage(self): - """ - returns class mediaFile of the first photo - """ - mediaFile = self.cardMedia.firstImage() - return mediaFile - - def firstVideo(self): - """ - returns class mediaFile of the first video - """ - mediaFile = self.cardMedia.firstVideo() - return mediaFile - - def handlePreferencesError(self, e, prefsFactory): - sys.stderr.write(_("Sorry,these preferences contain an error:\n")) - sys.stderr.write(prefsFactory.formatPreferencesForPrettyPrint() + "\n") - msg = str(e) - sys.stderr.write(msg + "\n") - - def initializeFromPrefs(self, notifyOnError): - """ - Setup thread so that user preferences are handled - """ - - def checkPrefs(prefsFactory): - try: - prefsFactory.checkPrefsForValidity() - except (rn.PrefValueInvalidError, rn.PrefLengthError, - rn.PrefValueKeyComboError, rn.PrefKeyError), e: - if notifyOnError: - self.handlePreferencesError(e, prefsFactory) - raise rn.PrefError - - self.prefs = self.parentApp.prefs - - #Image and Video filename preferences - sample_download_start_time = datetime.datetime.now() - - self.imageRenamePrefsFactory = rn.ImageRenamePreferences(self.prefs.image_rename, self, - self.fileSequenceLock, sequences) - self.imageRenamePrefsFactory.setDownloadStartTime(sample_download_start_time) - checkPrefs(self.imageRenamePrefsFactory) - - self.videoRenamePrefsFactory = rn.VideoRenamePreferences(self.prefs.video_rename, self, - self.fileSequenceLock, sequences) - self.videoRenamePrefsFactory.setDownloadStartTime(sample_download_start_time) - checkPrefs(self.videoRenamePrefsFactory) - - #Image and Video subfolder preferences - - self.subfolderPrefsFactory = rn.SubfolderPreferences(self.prefs.subfolder, self) - self.subfolderPrefsFactory.setDownloadStartTime(sample_download_start_time) - checkPrefs(self.subfolderPrefsFactory) - - self.videoSubfolderPrefsFactory = rn.VideoSubfolderPreferences(self.prefs.video_subfolder, self) - self.videoSubfolderPrefsFactory.setDownloadStartTime(sample_download_start_time) - checkPrefs(self.videoSubfolderPrefsFactory) - - # copy this variable, as it is used heavily in the loop - # and it is perhaps relatively expensive to read - self.stripCharacters = self.prefs.strip_characters - - def run(self): - """ - Copy photos from device to local drive, and if requested, backup - - 1. Should the image be downloaded? - 1.a generate file name - 1.a.1 generate sequence numbers if needed - 1.a.2 FIFO queue sequence numbers to indicate that they could - potentially be used in a filename - 1.b check to see if a file exists with the same name in the place it will - be downloaded to - 1.c if it exisits, and unique identifiers are not being used: - 1.b.1 if using sequence numbers or letters, then potentially any of the - sequence numbers in the queue could be used to make the filename - 1.b.1.a generate and check each filename using sequence numbers in the queue - 1.b.1.b if one of these filenames is unique, then image needs to be downloaded - 1.b.2 do not do not download - - - 2. Download the image - 2.a copy it to temporary folder (this takes time) - 2.b is the file name still unique? Perhaps a new file was created with this name in the meantime - (either by another thread or another program) - 2.b.1 don't allow any other thread to rename a file - 2.b.2 check file name - 2.b.3 adding suffix if it is not unique, being careful not to overwrite any existing file with a suffix - 2.b.4 rename it to the "real" name, effectively performing a mv - 2.b.5 allow other threads to rename files - - 3. Backup the image, using the same filename as was used when it was downloaded - 3.a does a file with the same name already exist on the backup medium? - 3.b if so, user preferences determine whether it should be overwritten or not - """ - - def checkDownloadPath(path): - """ - Checks to see if download folder exists. - - Creates it if it does not exist. - - Returns False if the path could not be created. - """ - - try: - if not os.path.isdir(path): - os.makedirs(path) - return True - - except: - display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) - msg = _("The following download path could not be created:\n") - msg += _("%(path)s: ") % {'path': path} - logError(config.CRITICAL_ERROR, _("Download cannot proceed"), msg) - cmd_line(_("Download cannot proceed")) - cmd_line(msg) - display_queue.put((self.parentApp.downloadFailed, (self.thread_id, ))) - display_queue.close("rw") - return False - - def getPrefs(notifyOnError): - try: - self.initializeFromPrefs(notifyOnError) - return True - except rn.PrefError: - if notifyOnError: - display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) - msg = _("There is an error in the program preferences.") - msg += _("\nPlease check preferences, restart the program, and try again.") - logError(config.CRITICAL_ERROR, _("Download cannot proceed"), msg) - cmd_line(_("Download cannot proceed")) - cmd_line(msg) - display_queue.put((self.parentApp.downloadFailed, (self.thread_id, ))) - display_queue.close("rw") - return False - - - - def scanMedia(): - """ - Scans media for photos and videos - """ - - # load images to display for when a thumbnail cannot be extracted or created - - self.photoThumbnail = getGenericPhotoImage() - self.videoThumbnail = getGenericVideoImage() - - imageRenameUsesJobCode = rn.usesJobCode(self.prefs.image_rename) - imageSubfolderUsesJobCode = rn.usesJobCode(self.prefs.subfolder) - videoRenameUsesJobCode = rn.usesJobCode(self.prefs.video_rename) - videoSubfolderUsesJobCode = rn.usesJobCode(self.prefs.video_subfolder) - - def loadFileMetadata(mediaFile): - """ - loads the metadate for the file, and additional information if required - """ - - problem = pn.Problem() - try: - mediaFile.loadMetadata() - except: - mediaFile.status = STATUS_CANNOT_DOWNLOAD - mediaFile.metadata = None - problem.add_problem(None, pn.CANNOT_DOWNLOAD_BAD_METADATA, {'filetype': mediaFile.displayNameCap}) - mediaFile.problem = problem - else: - # generate sample filename and subfolder - if mediaFile.isImage: - fallback_date = None - subfolderPrefsFactory = self.subfolderPrefsFactory - renamePrefsFactory = self.imageRenamePrefsFactory - nameUsesJobCode = imageRenameUsesJobCode - subfolderUsesJobCode = imageSubfolderUsesJobCode - else: - fallback_date = mediaFile.modificationTime - subfolderPrefsFactory = self.videoSubfolderPrefsFactory - renamePrefsFactory = self.videoRenamePrefsFactory - nameUsesJobCode = videoRenameUsesJobCode - subfolderUsesJobCode = videoSubfolderUsesJobCode - - generateSubfolderAndName(mediaFile, problem, subfolderPrefsFactory, renamePrefsFactory, - nameUsesJobCode, subfolderUsesJobCode, - self.prefs.strip_characters, fallback_date) - # generate thumbnail - mediaFile.generateThumbnail(self.videoTempWorkingDir) - - if mediaFile.thumbnail is None: - addGenericThumbnail(mediaFile) - - - def addGenericThumbnail(mediaFile): - """ - Adds a generic thumbnail to the mediafile, which - can be very useful when previews are disabled - """ - mediaFile.genericThumbnail = True - if mediaFile.isImage: - mediaFile.thumbnail = self.photoThumbnail - else: - mediaFile.thumbnail = self.videoThumbnail - - def downloadable(name): - isImage = media.isImage(name) - isVideo = media.isVideo(name) - download = (DOWNLOAD_VIDEO and (isImage or isVideo) or - ((not DOWNLOAD_VIDEO) and isImage)) - return (download, isImage, isVideo) - - def addFile(name, path, size, modificationTime, device, volume, isImage): - """ - Add an image or video to the list of scanned files to be shown to the user for potential downloading - """ - - if isImage: - downloadFolder = self.prefs.download_folder - else: - downloadFolder = self.prefs.video_download_folder - - mediaFile = media.MediaFile(self.thread_id, name, path, size, modificationTime, device, downloadFolder, volume, isImage) - loadFileMetadata(mediaFile) - # modificationTime is very useful for quick sorting - imagesAndVideos.append((mediaFile, modificationTime)) - display_queue.put((self.parentApp.addFile, (mediaFile,))) - - if isImage: - self.noImages += 1 - else: - self.noVideos += 1 - - - def gio_scan(path, fileSizeSum): - """recursive function to scan a directory and its subdirectories - for photos and possibly videos""" - - children = path.enumerate_children('standard::name,standard::type,standard::size,time::modified') - - for child in children: - if not self.running: - self.lock.acquire() - self.running = True - - if not self.ctrl: - return None - - if child.get_file_type() == gio.FILE_TYPE_DIRECTORY: - fileSizeSum = gio_scan(path.get_child(child.get_name()), fileSizeSum) - if fileSizeSum == None: - # this value will be None only if the thread is exiting - return None - elif child.get_file_type() == gio.FILE_TYPE_REGULAR: - name = child.get_name() - download, isImage, isVideo = downloadable(name) - if download: - size = child.get_size() - modificationTime = child.get_modification_time() - addFile(name, path.get_path(), size, modificationTime, self.cardMedia.prettyName(limit=0), self.cardMedia.volume, isImage) - fileSizeSum += size - - return fileSizeSum - - - imagesAndVideos = [] - fileSizeSum = 0 - self.noVideos = 0 - self.noImages = 0 - - if not using_gio or not self.cardMedia.volume: - for root, dirs, files in os.walk(self.cardMedia.getPath()): - for name in files: - if not self.running: - self.lock.acquire() - self.running = True - - if not self.ctrl: - return None - - - download, isImage, isVideo = downloadable(name) - if download: - fullFileName = os.path.join(root, name) - size = os.path.getsize(fullFileName) - modificationTime = os.path.getmtime(fullFileName) - addFile(name, root, size, modificationTime, self.cardMedia.prettyName(limit=0), self.cardMedia.volume, isImage) - fileSizeSum += size - - - else: - # using gio and have a volume - # make call to recursive function to scan volume - fileSizeSum = gio_scan(self.cardMedia.volume.volume.get_root(), fileSizeSum) - if fileSizeSum == None: - # thread exiting - return None - - # sort in place based on modification time - imagesAndVideos.sort(key=operator.itemgetter(1)) - noFiles = len(imagesAndVideos) - - self.scanComplete = True - - self.display_file_types = file_types_by_number(self.noImages, self.noVideos) - - - if noFiles: - self.cardMedia.setMedia(imagesAndVideos, fileSizeSum, noFiles) - # Translators: as already, mentioned the %s value should not be modified or left out. It may be moved if necessary. - # It refers to the actual number of photos that can be copied. For example, the user might see the following: - # '0 of 512 photos' or '0 of 10 videos' or '0 of 202 photos and videos'. - # This particular text is displayed to the user before the download has started. - display = _("%(number)s %(filetypes)s") % {'number':noFiles, 'filetypes':self.display_file_types} - display_queue.put((media_collection_treeview.updateCard, (self.thread_id, self.cardMedia.sizeOfImagesAndVideos()))) - display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, 0.0, display, 0))) - display_queue.put((self.parentApp.setDownloadButtonSensitivity, ())) - - # Translators: as you have already seen, the text can contain values that should not be modified or left out by you, for example %s. - # This text is another example of that, but it is is a little more complex. Here there are two values which will be displayed - # to the user when they run the program, signifying the number of photos found, and the device they were found on. - # %(number)s should be left exactly as is: 'number' should not be translated. The same applies to %(device)s: 'device' should - # not be translated. Generally speaking, if translating the sentence requires it, you can move items like '%(xyz)s' around - # in a sentence, but you should never modify them or leave them out. - cmd_line(_("Device scan complete: found %(number)s %(filetypes)s on %(device)s") % - {'number': noFiles, 'filetypes':self.display_file_types, - 'device': self.cardMedia.prettyName(limit=0)}) - return True - else: - # it might be better to display "0 of 0" here - display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) - cmd_line(_("Device scan complete: no %(filetypes)s found on %(device)s") % {'device':self.cardMedia.prettyName(limit=0), 'filetypes':self.types_searched_for}) - return False - - - def logError(severity, problem, details, resolution=None): - display_queue.put((log_dialog.addMessage, (self.thread_id, severity, problem, details, - resolution))) - if severity == config.WARNING: - self.noWarnings += 1 - else: - self.noErrors += 1 - - def notifyAndUnmount(umountAttemptOK): - if not self.cardMedia.volume: - unmountMessage = "" - notificationName = PROGRAM_NAME - else: - notificationName = self.cardMedia.volume.get_name() - if self.prefs.auto_unmount and umountAttemptOK: - self.cardMedia.volume.unmount(self.on_volume_unmount) - # This message informs the user that the device (e.g. camera, hard drive or memory card) was automatically unmounted and they can now remove it - unmountMessage = _("The device can now be safely removed") - else: - unmountMessage = "" - - file_types = file_types_by_number(noImagesDownloaded, noVideosDownloaded) - file_types_skipped = file_types_by_number(noImagesSkipped, noVideosSkipped) - message = _("%(noFiles)s %(filetypes)s downloaded") % {'noFiles':noFilesDownloaded, 'filetypes': file_types} - noFilesSkipped = noImagesSkipped + noVideosSkipped - if noFilesSkipped: - message += "\n" + _("%(noFiles)s %(filetypes)s failed to download") % {'noFiles':noFilesSkipped, 'filetypes':file_types_skipped} - - if self.noWarnings: - message = "%s\n%s " % (message, self.noWarnings) + _("warnings") - if self.noErrors: - message = "%s\n%s " % (message, self.noErrors) + _("errors") - - if unmountMessage: - message = "%s\n%s" % (message, unmountMessage) - - n = pynotify.Notification(notificationName, message) - - if self.cardMedia.volume: - icon = self.cardMedia.volume.get_icon_pixbuf(self.parentApp.notification_icon_size) - else: - icon = self.parentApp.application_icon - - n.set_icon_from_pixbuf(icon) - n.show() - - def createTempDir(baseDir): - """ - Create a temporary directory in which to download the photos to. - - Returns the directory if it was created, else returns None. - - Don't want to put it in system temp folder, as that is likely - to be on another partition and hence copying files from it - to the actual download folder will be slow!""" - try: - t = tempfile.mkdtemp(prefix='rapid-tmp-', - dir=baseDir) - return t - except OSError, (errno, strerror): - if not self.cardMedia.volume: - image_device = _("Source: %s\n") % self.cardMedia.getPath() - else: - _("Device: %s\n") % self.cardMedia.volume.get_name() - destination = _("Destination: %s") % baseDir - logError(config.CRITICAL_ERROR, _('Could not create temporary download directory'), - image_device + destination, - _("Download cannot proceed")) - cmd_line(_("Error:") + " " + _('Could not create temporary download directory')) - cmd_line(image_device + destination) - cmd_line(_("Download cannot proceed")) - display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) - display_queue.put((self.parentApp.downloadFailed, (self.thread_id, ))) - display_queue.close("rw") - self.running = False - self.lock.release() - return None - - def setupBackup(): - """ - Check for presence of backup path or volumes, and return the number of devices being used (1 in case of a path) - """ - no_devices = 0 - if self.prefs.backup_images: - no_devices = len(self.parentApp.backupVolumes) - if not self.prefs.backup_device_autodetection: - if not os.path.isdir(self.prefs.backup_location): - # the user has manually specified a path, but it - # does not exist. This is a problem. - try: - os.makedirs(self.prefs.backup_location) - except: - logError(config.SERIOUS_ERROR, _("Backup path does not exist"), - _("The path %s could not be created") % path, - _("No backups can occur") - ) - no_devices = 0 - return no_devices - - def checkIfNeedAJobCode(): - needAJobCode = NeedAJobCode(self.prefs) - - for f in self.cardMedia.imagesAndVideos: - mediaFile = f[0] - if mediaFile.status in [STATUS_WARNING, STATUS_NOT_DOWNLOADED]: - if needAJobCode.needAJobCode(mediaFile.jobcode, mediaFile.isImage): - return True - return False - - def createBothTempDirs(): - self.photoTempWorkingDir = createTempDir(photoBaseDownloadDir) - created = self.photoTempWorkingDir is not None - if created and DOWNLOAD_VIDEO: - self.videoTempWorkingDir = createTempDir(videoBaseDownloadDir) - created = self.videoTempWorkingDir is not None - - return created - - - def checkProblemWithNameGeneration(mediaFile): - if mediaFile.problem.has_problem(): - logError(config.WARNING, - mediaFile.problem.get_title(), - _("Source: %(source)s\nDestination: %(destination)s\n%(problem)s") % - {'source': mediaFile.fullFileName, 'destination': mediaFile.downloadFullFileName, 'problem': mediaFile.problem.get_problems()}) - mediaFile.status = STATUS_DOWNLOADED_WITH_WARNING - - def fileAlreadyExists(mediaFile, identifier=None): - """ Notify the user that the photo or video could not be downloaded because it already exists""" - - # get information on when the existing file was last modified - try: - modificationTime = os.path.getmtime(mediaFile.downloadFullFileName) - dt = datetime.datetime.fromtimestamp(modificationTime) - date = dt.strftime("%x") - time = dt.strftime("%X") - except: - sys.stderr.write("WARNING: could not determine the file modification time of an existing file\n") - date = time = '' - - if not identifier: - mediaFile.problem.add_problem(None, pn.FILE_ALREADY_EXISTS_NO_DOWNLOAD, {'filetype':mediaFile.displayNameCap}) - mediaFile.problem.add_extra_detail(pn.EXISTING_FILE, {'filetype': mediaFile.displayName, 'date': date, 'time': time}) - mediaFile.status = STATUS_DOWNLOAD_FAILED - log_status = config.SERIOUS_ERROR - problem_text = pn.extra_detail_definitions[pn.EXISTING_FILE] % {'date':date, 'time':time, 'filetype': mediaFile.displayName} - else: - mediaFile.problem.add_problem(None, pn.UNIQUE_IDENTIFIER_ADDED, {'filetype':mediaFile.displayNameCap}) - mediaFile.problem.add_extra_detail(pn.UNIQUE_IDENTIFIER, {'identifier': identifier, 'filetype': mediaFile.displayName, 'date': date, 'time': time}) - mediaFile.status = STATUS_DOWNLOADED_WITH_WARNING - log_status = config.WARNING - problem_text = pn.extra_detail_definitions[pn.UNIQUE_IDENTIFIER] % {'identifier': identifier, 'filetype': mediaFile.displayName, 'date': date, 'time': time} - - logError(log_status, mediaFile.problem.get_title(), - _("Source: %(source)s\nDestination: %(destination)s") - % {'source': mediaFile.fullFileName, 'destination': mediaFile.downloadFullFileName}, - problem_text) - - def downloadCopyingError(mediaFile, inst=None, errno=None, strerror=None): - """Notify the user that an error occurred (most likely at the OS / filesystem level) when coyping a photo or video""" - - if errno != None and strerror != None: - mediaFile.problem.add_problem(None, pn.DOWNLOAD_COPYING_ERROR_W_NO, {'filetype': mediaFile.displayName}) - mediaFile.problem.add_extra_detail(pn.DOWNLOAD_COPYING_ERROR_W_NO_DETAIL, {'errorno': errno, 'strerror': strerror}) - - else: - mediaFile.problem.add_problem(None, pn.DOWNLOAD_COPYING_ERROR, {'filetype': mediaFile.displayName}) - if not inst: - # hopefully inst will never be None, but just to be safe... - inst = _("Please check your system and try again.") - mediaFile.problem.add_extra_detail(pn.DOWNLOAD_COPYING_ERROR_DETAIL, inst) - - logError(config.SERIOUS_ERROR, mediaFile.problem.get_title(), mediaFile.problem.get_problems()) - mediaFile.status = STATUS_DOWNLOAD_FAILED - - def sameNameDifferentExif(image_name, mediaFile): - """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 = downloaded_files.extExifDateTime(image_name) - detail = {'image1': "%s%s" % (image_name, i1_ext), - 'image1_date': i1_date_time.strftime("%x"), - 'image1_time': time_subseconds_human_readable(i1_date_time, i1_subseconds), - 'image2': mediaFile.name, - 'image2_date': mediaFile.metadata.dateTime().strftime("%x"), - 'image2_time': time_subseconds_human_readable( - mediaFile.metadata.dateTime(), - mediaFile.metadata.subSeconds())} - mediaFile.problem.add_problem(None, pn.SAME_FILE_DIFFERENT_EXIF, detail) - - msg = pn.problem_definitions[pn.SAME_FILE_DIFFERENT_EXIF][1] % detail - logError(config.WARNING,_('Photos detected with the same filenames, but taken at different times'), msg) - mediaFile.status = STATUS_DOWNLOADED_WITH_WARNING - - def generateSubfolderAndFileName(mediaFile): - """ - Generates subfolder and file names for photos and videos - """ - - skipFile = alreadyDownloaded = False - sequence_to_use = None - - if mediaFile.isVideo: - fileRenameFactory = self.videoRenamePrefsFactory - subfolderFactory = self.videoSubfolderPrefsFactory - else: - # file is an photo - fileRenameFactory = self.imageRenamePrefsFactory - subfolderFactory = self.subfolderPrefsFactory - - fileRenameFactory.setJobCode(mediaFile.jobcode) - subfolderFactory.setJobCode(mediaFile.jobcode) - - mediaFile.problem = pn.Problem() - subfolderFactory.initializeProblem(mediaFile.problem) - fileRenameFactory.initializeProblem(mediaFile.problem) - - # Here we cannot assume that the subfolder value will contain something -- the user may have changed the preferences after the scan - mediaFile.downloadSubfolder = subfolderFactory.generateNameUsingPreferences( - mediaFile.metadata, mediaFile.name, - self.stripCharacters, fallback_date = mediaFile.modificationTime) - - - if self.prefs.synchronize_raw_jpg and usesImageSequenceElements and mediaFile.isImage: - #synchronizing RAW and JPEG only applies to photos, not videos - image_name, image_ext = os.path.splitext(mediaFile.name) - with self.downloadedFilesLock: - i, sequence_to_use = downloaded_files.matching_pair(image_name, image_ext, mediaFile.metadata.dateTime(), mediaFile.metadata.subSeconds()) - if i == -1: - # this exact file has already been downloaded (same extension, same filename, and same exif date time subsecond info) - if not addUniqueIdentifier: - logError(config.SERIOUS_ERROR,_('Photo has already been downloaded'), - _("Source: %(source)s") % {'source': mediaFile.fullFileName}) - mediaFile.problem.add_problem(None, pn.FILE_ALREADY_DOWNLOADED, {'filetype': mediaFile.displayNameCap}) - skipFile = True - - - # pass the subfolder the image will go into, as this is needed to determine subfolder sequence numbers - # indicate that sequences chosen should be queued - - if not skipFile: - mediaFile.downloadName = fileRenameFactory.generateNameUsingPreferences( - mediaFile.metadata, mediaFile.name, self.stripCharacters, mediaFile.downloadSubfolder, - sequencesPreliminary = True, - sequence_to_use = sequence_to_use, - fallback_date = mediaFile.modificationTime) - - mediaFile.downloadPath = os.path.join(mediaFile.downloadFolder, mediaFile.downloadSubfolder) - mediaFile.downloadFullFileName = os.path.join(mediaFile.downloadPath, mediaFile.downloadName) - - if not mediaFile.downloadName or not mediaFile.downloadSubfolder: - if not mediaFile.downloadName and not mediaFile.downloadSubfolder: - area = _("subfolder and filename") - elif not mediaFile.downloadName: - area = _("filename") - else: - area = _("subfolder") - problem.add_problem(None, pn.ERROR_IN_NAME_GENERATION, {'filetype': mediaFile.displayNameCap, 'area': area}) - problem.add_extra_detail(pn.NO_DATA_TO_NAME, {'filetype': area}) - skipFile = True - logError(config.SERIOUS_ERROR, pn.problem_definitions[ERROR_IN_NAME_GENERATION][1] % {'filetype': mediaFile.displayNameCap, 'area': area}) - - if not skipFile: - checkProblemWithNameGeneration(mediaFile) - else: - self.sizeDownloaded += mediaFile.size * (no_backup_devices + 1) - mediaFile.status = STATUS_DOWNLOAD_FAILED - - return (skipFile, sequence_to_use) - - def progress_callback(amount_downloaded, total): - if (amount_downloaded - self.bytes_downloaded > 2097152) or (amount_downloaded == total): - chunk_downloaded = amount_downloaded - self.bytes_downloaded - self.bytes_downloaded = amount_downloaded - percentComplete = (float(self.sizeDownloaded + amount_downloaded) / sizeFiles) * 100 - - display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, percentComplete, None, chunk_downloaded))) - - def downloadFile(mediaFile, sequence_to_use): - """ - Downloads the photo or video file to the specified subfolder - """ - - if not mediaFile.isImage: - renameFactory = self.videoRenamePrefsFactory - else: - renameFactory = self.imageRenamePrefsFactory - - def progress_callback_no_update(amount_downloaded, total): - pass - - try: - fileDownloaded = False - if not os.path.isdir(mediaFile.downloadPath): - os.makedirs(mediaFile.downloadPath) - - nameUniqueBeforeCopy = True - downloadNonUniqueFile = True - - # do a preliminary check to see if a file with the same name already exists - if os.path.exists(mediaFile.downloadFullFileName): - nameUniqueBeforeCopy = False - if not addUniqueIdentifier: - downloadNonUniqueFile = False - if (usesVideoSequenceElements and not mediaFile.isImage) or (usesImageSequenceElements and mediaFile.isImage and not self.prefs.synchronize_raw_jpg): - # potentially, a unique file name could still be generated - # investigate this possibility - with self.fileSequenceLock: - for possibleName in renameFactory.generateNameSequencePossibilities( - mediaFile.metadata, - mediaFile.name, self.stripCharacters, mediaFile.downloadSubfolder, - fallback_date = mediaFile.modificationTime): - if possibleName: - # no need to check for any problems here, it's just a temporary name - possibleFile = os.path.join(mediaFile.downloadPath, possibleName) - possibleTempFile = os.path.join(tempWorkingDir, possibleName) - if not os.path.exists(possibleFile) and not os.path.exists(possibleTempFile): - downloadNonUniqueFile = True - break - - - if not downloadNonUniqueFile: - fileAlreadyExists(mediaFile) - - copy_succeeded = False - if nameUniqueBeforeCopy or downloadNonUniqueFile: - tempWorkingfile = os.path.join(tempWorkingDir, mediaFile.downloadName) - if using_gio: - g_dest = gio.File(path=tempWorkingfile) - g_src = gio.File(path=mediaFile.fullFileName) - try: - if not g_src.copy(g_dest, progress_callback, cancellable=gio.Cancellable()): - downloadCopyingError(mediaFile) - else: - copy_succeeded = True - except glib.GError, inst: - downloadCopyingError(mediaFile, inst=inst) - else: - shutil.copy2(mediaFile.fullFileName, tempWorkingfile) - copy_succeeded = True - - if copy_succeeded: - with self.fileRenameLock: - doRename = True - if usesSequenceElements: - with self.fileSequenceLock: - # get a filename and use this as the "real" filename - if sequence_to_use is None and self.prefs.synchronize_raw_jpg and mediaFile.isImage: - # must check again, just in case the matching pair has been downloaded in the meantime - image_name, image_ext = os.path.splitext(mediaFile.name) - with self.downloadedFilesLock: - i, sequence_to_use = downloaded_files.matching_pair(image_name, image_ext, mediaFile.metadata.dateTime(), mediaFile.metadata.subSeconds()) - if i == -99: - sameNameDifferentExif(image_name, mediaFile) - - mediaFile.downloadName = renameFactory.generateNameUsingPreferences( - mediaFile.metadata, mediaFile.name, self.stripCharacters, mediaFile.downloadSubfolder, - sequencesPreliminary = False, - sequence_to_use = sequence_to_use, - fallback_date = mediaFile.modificationTime) - - if not mediaFile.downloadName: - # there was a serious error generating the filename - doRename = False - else: - mediaFile.downloadFullFileName = os.path.join(mediaFile.downloadPath, mediaFile.downloadName) - # check if the file exists again - if os.path.exists(mediaFile.downloadFullFileName): - if not addUniqueIdentifier: - doRename = False - fileAlreadyExists(mediaFile) - else: - # add basic suffix to make the filename unique - name = os.path.splitext(mediaFile.downloadName) - suffixAlreadyUsed = True - while suffixAlreadyUsed: - if mediaFile.downloadFullFileName in duplicate_files: - duplicate_files[mediaFile.downloadFullFileName] += 1 - else: - duplicate_files[mediaFile.downloadFullFileName] = 1 - identifier = '_%s' % duplicate_files[mediaFile.downloadFullFileName] - mediaFile.downloadName = name[0] + identifier + name[1] - possibleNewFile = os.path.join(mediaFile.downloadPath, mediaFile.downloadName) - suffixAlreadyUsed = os.path.exists(possibleNewFile) - - fileAlreadyExists(mediaFile, identifier) - mediaFile.downloadFullFileName = possibleNewFile - - - if doRename: - rename_succeeded = False - if using_gio: - g_dest = gio.File(path=mediaFile.downloadFullFileName) - g_src = gio.File(path=tempWorkingfile) - try: - if not g_src.move(g_dest, progress_callback_no_update, cancellable=gio.Cancellable()): - downloadCopyingError(mediaFile) - else: - rename_succeeded = True - except glib.GError, inst: - downloadCopyingError(mediaFile, inst=inst) - else: - os.rename(tempWorkingfile, mediaFile.downloadFullFileName) - rename_succeeded = True - - if rename_succeeded: - fileDownloaded = True - if mediaFile.status != STATUS_DOWNLOADED_WITH_WARNING: - mediaFile.status = STATUS_DOWNLOADED - if usesImageSequenceElements: - if self.prefs.synchronize_raw_jpg and mediaFile.isImage: - name, ext = os.path.splitext(mediaFile.name) - if sequence_to_use is None: - with self.fileSequenceLock: - seq = renameFactory.sequences.getFinalSequence() - else: - seq = sequence_to_use - with self.downloadedFilesLock: - downloaded_files.add_download(name, ext, mediaFile.metadata.dateTime(), mediaFile.metadata.subSeconds(), seq) - - - with self.fileSequenceLock: - if sequence_to_use is None: - renameFactory.sequences.imageCopySucceeded() - if usesStoredSequenceNo: - self.prefs.stored_sequence_no += 1 - - with self.fileSequenceLock: - if sequence_to_use is None: - if self.prefs.incrementDownloadsToday(): - # A new day, according the user's preferences of what time a day begins, has started - cmd_line(_("New day has started - resetting 'Downloads Today' sequence number")) - - sequences.setDownloadsToday(0) - - except (IOError, OSError), (errno, strerror): - downloadCopyingError(mediaFile, errno=errno, strerror=strerror) - - if usesSequenceElements: - if not fileDownloaded and sequence_to_use is None: - renameFactory.sequences.imageCopyFailed() - - #update record keeping using in tracking progress - self.sizeDownloaded += mediaFile.size - self.bytes_downloaded_in_download = self.bytes_downloaded - - return fileDownloaded - - - def backupFile(mediaFile, fileDownloaded, no_backup_devices): - """ - Backup photo or video to path(s) chosen by the user - - there are three scenarios: - (1) file has just been downloaded and should now be backed up - (2) file was already downloaded on some previous occassion and should still be backed up, because it hasn't been yet - (3) file has been backed up already (or at least, a file with the same name already exists) - - A backup medium can be used to backup photos or videos, or both. - """ - - backed_up = False - fileNotBackedUpMessageDisplayed = False - error_encountered = False - expected_bytes_downloaded = self.sizeDownloaded + no_backup_devices * mediaFile.size - - if no_backup_devices: - for rootBackupDir in self.parentApp.backupVolumes: - self.bytes_downloaded = 0 - if self.prefs.backup_device_autodetection: - volume = self.parentApp.backupVolumes[rootBackupDir].get_name() - if mediaFile.isImage: - backupDir = os.path.join(rootBackupDir, self.prefs.backup_identifier) - else: - backupDir = os.path.join(rootBackupDir, self.prefs.video_backup_identifier) - else: - # photos and videos will be backed up into the same root folder, which the user has manually specified - backupDir = rootBackupDir - volume = backupDir # os.path.split(backupDir)[1] - - # if user has chosen auto detection, then: - # photos should only be backed up to photo backup locations - # videos should only be backed up to video backup locations - # if user did not choose autodetection, and the backup path doesn't exist, then - # will try to create it - if os.path.isdir(backupDir) or not self.prefs.backup_device_autodetection: - - backupPath = os.path.join(backupDir, mediaFile.downloadSubfolder) - newBackupFile = os.path.join(backupPath, mediaFile.downloadName) - copyBackup = True - if os.path.exists(newBackupFile): - # this check is of course not thread safe -- it doesn't need to be, because at this stage the file names are going to be unique - # (the folder structure is the same as the actual download folders, and the file names are unique in them) - copyBackup = self.prefs.backup_duplicate_overwrite - - if copyBackup: - mediaFile.problem.add_problem(None, pn.BACKUP_EXISTS_OVERWRITTEN, volume) - else: - mediaFile.problem.add_problem(None, pn.BACKUP_EXISTS, volume) - severity = config.SERIOUS_ERROR - fileNotBackedUpMessageDisplayed = True - - title = _("Backup of %(file_type)s already exists") % {'file_type': mediaFile.displayName} - details = _("Source: %(source)s\nDestination: %(destination)s") \ - % {'source': mediaFile.fullFileName, 'destination': newBackupFile} - if copyBackup: - resolution = _("Backup %(file_type)s overwritten") % {'file_type': mediaFile.displayName} - else: - if self.prefs.backup_device_autodetection: - volume = self.parentApp.backupVolumes[rootBackupDir].get_name() - resolution = _("%(file_type)s not backed up to %(volume)s") % {'file_type': mediaFile.displayNameCap, 'volume': volume} - else: - resolution = _("%(file_type)s not backed up") % {'file_type': mediaFile.displayNameCap} - logError(severity, title, details, resolution) - - if copyBackup: - if fileDownloaded: - fileToCopy = mediaFile.downloadFullFileName - else: - fileToCopy = mediaFile.fullFileName - if os.path.isdir(backupPath): - pathExists = True - else: - pathExists = False - # create the backup subfolders - if using_gio: - dirs = gio.File(backupPath) - try: - if dirs.make_directory_with_parents(cancellable=gio.Cancellable()): - pathExists = True - except glib.GError, inst: - fileNotBackedUpMessageDisplayed = True - mediaFile.problem.add_problem(None, pn.BACKUP_DIRECTORY_CREATION, volume) - mediaFile.problem.add_extra_detail('%s%s' % (pn.BACKUP_DIRECTORY_CREATION, volume), inst) - error_encountered = True - logError(config.SERIOUS_ERROR, _('Backing up error'), - _("Destination directory could not be created: %(directory)s\n") % - {'directory': backupPath, } + - _("Source: %(source)s\nDestination: %(destination)s") % - {'source': mediaFile.fullFileName, 'destination': newBackupFile} + "\n" + - _("Error: %(inst)s") % {'inst': inst}, - _('The %(file_type)s was not backed up.') % {'file_type': mediaFile.displayName} - ) - else: - # recreate folder structure in backup location - # cannot do os.makedirs(backupPath) - it can give bad results when using external drives - # we know backupDir exists - # all the components of subfolder may not - folders = mediaFile.downloadSubfolder.split(os.path.sep) - folderToMake = backupDir - for f in folders: - if f: - folderToMake = os.path.join(folderToMake, f) - if not os.path.isdir(folderToMake): - try: - os.mkdir(folderToMake) - pathExists = True - except (IOError, OSError), (errno, strerror): - fileNotBackedUpMessageDisplayed = True - inst = "%s: %s" % (errno, strerror) - mediaFile.problem.add_problem(None, pn.BACKUP_DIRECTORY_CREATION, volume) - mediaFile.problem.add_extra_detail('%s%s' % (pn.BACKUP_DIRECTORY_CREATION, volume), inst) - error_encountered = True - logError(config.SERIOUS_ERROR, _('Backing up error'), - _("Destination directory could not be created: %(directory)s\n") % - {'directory': backupPath, } + - _("Source: %(source)s\nDestination: %(destination)s") % - {'source': mediaFile.fullFileName, 'destination': newBackupFile} + "\n" + - _("Error: %(errno)s %(strerror)s") % {'errno': errno, 'strerror': strerror}, - _('The %(file_type)s was not backed up.') % {'file_type': mediaFile.displayName} - ) - - break - - if pathExists: - if using_gio: - g_dest = gio.File(path=newBackupFile) - g_src = gio.File(path=fileToCopy) - if self.prefs.backup_duplicate_overwrite: - flags = gio.FILE_COPY_OVERWRITE - else: - flags = gio.FILE_COPY_NONE - try: - if not g_src.copy(g_dest, progress_callback, flags, cancellable=gio.Cancellable()): - fileNotBackedUpMessageDisplayed = True - mediaFile.problem.add_problem(None, pn.BACKUP_ERROR, volume) - error_encountered = True - else: - backed_up = True - if mediaFile.status == STATUS_DOWNLOAD_FAILED: - mediaFile.problem.add_problem(None, pn.NO_DOWNLOAD_WAS_BACKED_UP, volume) - except glib.GError, inst: - fileNotBackedUpMessageDisplayed = True - mediaFile.problem.add_problem(None, pn.BACKUP_ERROR, volume) - mediaFile.problem.add_extra_detail('%s%s' % (pn.BACKUP_ERROR, volume), inst) - error_encountered = True - logError(config.SERIOUS_ERROR, _('Backing up error'), - _("Source: %(source)s\nDestination: %(destination)s") % - {'source': fileToCopy, 'destination': newBackupFile} + "\n" + - _("Error: %(inst)s") % {'inst': inst}, - _('The %(file_type)s was not backed up.') % {'file_type': mediaFile.displayName} - ) - else: - try: - shutil.copy2(fileToCopy, newBackupFile) - backed_up = True - if mediaFile.status == STATUS_DOWNLOAD_FAILED: - mediaFile.problem.add_problem(None, pn.NO_DOWNLOAD_WAS_BACKED_UP, volume) - - except (IOError, OSError), (errno, strerror): - fileNotBackedUpMessageDisplayed = True - mediaFile.problem.add_problem(None, pn.BACKUP_ERROR, volume) - inst = "%s: %s" % (errno, strerror) - mediaFile.problem.add_extra_detail('%s%s' % (pn.BACKUP_ERROR, volume), inst) - error_encountered = True - logError(config.SERIOUS_ERROR, _('Backing up error'), - _("Source: %(source)s\nDestination: %(destination)s") % - {'source': fileToCopy, 'destination': newBackupFile} + "\n" + - _("Error: %(errno)s %(strerror)s") % {'errno': errno, 'strerror': strerror}, - _('The %(file_type)s was not backed up.') % {'file_type': mediaFile.displayName} - ) - - #update record keeping using in tracking progress - self.sizeDownloaded += mediaFile.size - self.bytes_downloaded_in_backup += self.bytes_downloaded - - if not backed_up and not fileNotBackedUpMessageDisplayed: - # The file has not been backed up to any medium - mediaFile.problem.add_problem(None, pn.NO_BACKUP_PERFORMED, {'filetype': mediaFile.displayNameCap}) - - severity = config.SERIOUS_ERROR - problem = _("%(file_type)s could not be backed up") % {'file_type': mediaFile.displayName} - details = _("Source: %(source)s") % {'source': mediaFile.fullFileName} - if self.prefs.backup_device_autodetection: - resolution = _("No suitable backup volume was found") - else: - resolution = _("A backup location was not found") - logError(severity, problem, details, resolution) - - if backed_up and mediaFile.status == STATUS_DOWNLOAD_FAILED: - mediaFile.problem.add_extra_detail(pn.BACKUP_OK_TYPE, mediaFile.displayNameCap) - - if not backed_up: - if mediaFile.status == STATUS_DOWNLOAD_FAILED: - mediaFile.status = STATUS_DOWNLOAD_AND_BACKUP_FAILED - else: - mediaFile.status = STATUS_BACKUP_PROBLEM - elif error_encountered: - # it was backed up to at least one volume, but there was an error on another backup volume - if mediaFile.status != STATUS_DOWNLOAD_FAILED: - mediaFile.status = STATUS_BACKUP_PROBLEM - - # Take into account instances where a backup device has been removed part way through a download - # (thereby making self.parentApp.backupVolumes have less items than expected) - if self.sizeDownloaded < expected_bytes_downloaded: - self.sizeDownloaded = expected_bytes_downloaded - return backed_up - - - self.hasStarted = True - display_queue.open('w') - - #Do not try to handle any preference errors here - getPrefs(False) - - #Check photo and video download path, create if necessary - photoBaseDownloadDir = self.prefs.download_folder - if not checkDownloadPath(photoBaseDownloadDir): - return # cleanup already done - - if DOWNLOAD_VIDEO: - videoBaseDownloadDir = self.prefs.video_download_folder - if not checkDownloadPath(videoBaseDownloadDir): - return - else: - videoBaseDownloadDir = self.videoTempWorkingDir = None - - if not createBothTempDirs(): - return - - s = scanMedia() - if s is None: - if not self.ctrl: - self.running = False - display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) - display_queue.close("rw") - return - else: - sys.stderr.write("FIXME: scan returned None, but the thread is not meant to be exiting\n") - if not s: - cmd_line(_("This device has no %(types_searched_for)s to download from.") % {'types_searched_for': self.types_searched_for}) - display_queue.put((self.parentApp.downloadFailed, (self.thread_id, ))) - display_queue.close("rw") - self.running = False - return - - if self.scanResultsStale or self.scanResultsStaleDownloadFolder: - display_queue.put((self.parentApp.regenerateScannedDevices, (self.thread_id, ))) - all_files_downloaded = False - - totalNonErrorFiles = self.cardMedia.numberOfFilesNotCannotDownload() - - if not self.autoStart: - # halt thread, waiting to be restarted so download proceeds - self.cleanUp() - self.running = False - self.lock.acquire() - - if not self.ctrl: - # thread will restart at this point, when the program is exiting - # so must exit if self.ctrl indicates this - - self.running = False - display_queue.close("rw") - return - - self.running = True - if not createBothTempDirs(): - return - - else: - if need_job_code_for_renaming: - if checkIfNeedAJobCode(): - if job_code == None: - self.cleanUp() - self.waitingForJobCode = True - display_queue.put((self.parentApp.getJobCode, ())) - self.running = False - self.lock.acquire() - - if not self.ctrl: - # thread is exiting - display_queue.close("rw") - return - - self.running = True - self.waitingForJobCode = False - if not createBothTempDirs(): - return - else: - # User has entered a job code, and it's in the global variable - # Assign it to all those files that do not have one - display_queue.put((self.parentApp.selection_vbox.selection_treeview.apply_job_code, (job_code, False, True, self.thread_id))) - - # auto start could be false if the user hit cancel when prompted for a job code - if self.autoStart: - # set all in this thread to download pending - display_queue.put((self.parentApp.selection_vbox.selection_treeview.set_status_to_download_pending, (False, self.thread_id))) - # wait until all the files have had their status set to download pending, and once that is done, restart - self.running = False - self.lock.acquire() - self.running = True - - # set download started time - display_queue.put((self.parentApp.setDownloadStartTime, ())) - - while not all_files_downloaded: - - # set the download start time to be the time that the user clicked the download button, or if on auto start, the value just set - i = 0 - while self.parentApp.download_start_time is None or i > 2: - time.sleep(0.5) - i += 1 - - if self.parentApp.download_start_time: - start_time = self.parentApp.download_start_time - else: - # in a bizarre corner case situation, with mulitple cards of greatly varying size, - # it's possible the start time was set above and then in the meantime unset (very unlikely, but conceivably it could happen) - # fall back to the current time in this less than satisfactory situation - start_time = datetime.datetime.now() - - self.imageRenamePrefsFactory.setDownloadStartTime(start_time) - self.subfolderPrefsFactory.setDownloadStartTime(start_time) - if DOWNLOAD_VIDEO: - self.videoRenamePrefsFactory.setDownloadStartTime(start_time) - self.videoSubfolderPrefsFactory.setDownloadStartTime(start_time) - - self.noErrors = self.noWarnings = 0 - - if not getPrefs(True): - self.running = False - display_queue.close("rw") - return - - self.downloadStarted = True - cmd_line(_("Download has started from %s") % self.cardMedia.prettyName(limit=0)) - - - noFiles, sizeFiles, fileIndex = self.cardMedia.sizeAndNumberDownloadPending() - cmd_line(_("Attempting to download %s files") % noFiles) - - no_backup_devices = setupBackup() - - # include the time it takes to copy to the backup volumes - sizeFiles = sizeFiles * (no_backup_devices + 1) - - display_queue.put((self.parentApp.timeRemaining.set, (self.thread_id, sizeFiles))) - - i = 0 - self.sizeDownloaded = noFilesDownloaded = noImagesDownloaded = noVideosDownloaded = noImagesSkipped = noVideosSkipped = 0 - filesDownloadedSuccessfully = [] - self.bytes_downloaded_in_backup = 0 - - display_queue.put((self.parentApp.addToTotalDownloadSize, (sizeFiles, ))) - display_queue.put((self.parentApp.setOverallDownloadMark, ())) - display_queue.put((self.parentApp.postStartDownloadTasks, ())) - - sizeFiles = float(sizeFiles) - - addUniqueIdentifier = self.prefs.download_conflict_resolution == config.ADD_UNIQUE_IDENTIFIER - usesImageSequenceElements = self.imageRenamePrefsFactory.usesSequenceElements() - usesVideoSequenceElements = self.videoRenamePrefsFactory.usesSequenceElements() - usesSequenceElements = usesVideoSequenceElements or usesImageSequenceElements - - usesStoredSequenceNo = (self.imageRenamePrefsFactory.usesTheSequenceElement(rn.STORED_SEQ_NUMBER) or - self.videoRenamePrefsFactory.usesTheSequenceElement(rn.STORED_SEQ_NUMBER)) - sequences.setUseOfSequenceElements( - self.imageRenamePrefsFactory.usesTheSequenceElement(rn.SESSION_SEQ_NUMBER), - self.imageRenamePrefsFactory.usesTheSequenceElement(rn.SEQUENCE_LETTER)) - - # reset the progress bar to update the status of this download attempt - progressBarText = _("%(number)s of %(total)s %(filetypes)s") % {'number': 0, 'total': noFiles, 'filetypes':self.display_file_types} - display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, 0.0, progressBarText, 0))) - - - while i < noFiles: - # if the user pauses the download, then this will be triggered - if not self.running: - self.lock.acquire() - self.running = True - - if not self.ctrl: - self.running = False - self.cleanUp() - display_queue.close("rw") - return - - # get information about the image to deduce image name and path - mediaFile = self.cardMedia.imagesAndVideos[fileIndex[i]][0] - if not mediaFile.status == STATUS_DOWNLOAD_PENDING: - sys.stderr.write("FIXME: Thread %s is trying to download a file that it should not be!!" % self.thread_id) - else: - self.bytes_downloaded_in_download = self.bytes_downloaded_in_backup = self.bytes_downloaded = 0 - if mediaFile.isImage: - tempWorkingDir = self.photoTempWorkingDir - baseDownloadDir = photoBaseDownloadDir - else: - tempWorkingDir = self.videoTempWorkingDir - baseDownloadDir = videoBaseDownloadDir - - skipFile, sequence_to_use = generateSubfolderAndFileName(mediaFile) - - if skipFile: - if mediaFile.isImage: - noImagesSkipped += 1 - else: - noVideosSkipped += 1 - else: - fileDownloaded = downloadFile(mediaFile, sequence_to_use) - - if self.prefs.backup_images: - backed_up = backupFile(mediaFile, fileDownloaded, no_backup_devices) - - if fileDownloaded: - noFilesDownloaded += 1 - if mediaFile.isImage: - noImagesDownloaded += 1 - else: - noVideosDownloaded += 1 - if self.prefs.backup_images and backed_up: - filesDownloadedSuccessfully.append(mediaFile.fullFileName) - elif not self.prefs.backup_images: - filesDownloadedSuccessfully.append(mediaFile.fullFileName) - else: - if mediaFile.isImage: - noImagesSkipped += 1 - else: - noVideosSkipped += 1 - - #update the selction treeview in the main window with the new status of the file - display_queue.put((self.parentApp.update_status_post_download, (mediaFile.treerowref, ))) - - percentComplete = (float(self.sizeDownloaded) / sizeFiles) * 100 - - if self.sizeDownloaded == sizeFiles and (totalNonErrorFiles - noFiles): - progressBarText = _("%(number)s of %(total)s %(filetypes)s (%(remaining)s remaining)") % { - 'number': i + 1, 'total': noFiles, 'filetypes':self.display_file_types, - 'remaining': totalNonErrorFiles - noFiles} - else: - progressBarText = _("%(number)s of %(total)s %(filetypes)s") % {'number': i + 1, 'total': noFiles, 'filetypes':self.display_file_types} - - if using_gio: - # do not want to update the progress bar any more than it has already been updated - size = mediaFile.size * (no_backup_devices + 1) - self.bytes_downloaded_in_download - self.bytes_downloaded_in_backup - else: - size = mediaFile.size * (no_backup_devices + 1) - display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, percentComplete, progressBarText, size))) - - i += 1 - - with self.statsLock: - self.downloadStats.adjust(self.sizeDownloaded, noImagesDownloaded, noVideosDownloaded, noImagesSkipped, noVideosSkipped, self.noWarnings, self.noErrors) - - if self.prefs.auto_delete: - j = 0 - for imageOrVideo in filesDownloadedSuccessfully: - try: - os.unlink(imageOrVideo) - j += 1 - except OSError, (errno, strerror): - logError(config.SERIOUS_ERROR, _("Could not delete photo or video from device"), - _("Photo: %(source)s\nError: %(errno)s %(strerror)s") - % {'source': image, 'errno': errno, 'strerror': strerror}) - except: - logError(config.SERIOUS_ERROR, _("Could not delete photo or video from device"), - _("Photo: %(source)s")) - - cmd_line(_("Deleted %(number)i %(filetypes)s from device") % {'number':j, 'filetypes':self.display_file_types}) - - totalNonErrorFiles = totalNonErrorFiles - noFiles - if totalNonErrorFiles == 0: - all_files_downloaded = True - - # must manually delete these variables, or else the media cannot be unmounted (bug in some versions of pyexiv2 / exiv2) - # for some reason directories on the device remain open with read only access, even after these steps - I don't know why - del self.subfolderPrefsFactory, self.imageRenamePrefsFactory, self.videoSubfolderPrefsFactory, self.videoRenamePrefsFactory - for i in self.cardMedia.imagesAndVideos: - i[0].metadata = None - - notifyAndUnmount(umountAttemptOK = all_files_downloaded) - cmd_line(_("Download complete from %s") % self.cardMedia.prettyName(limit=0)) - display_queue.put((self.parentApp.notifyUserAllDownloadsComplete,())) - display_queue.put((self.parentApp.resetSequences,())) - - if all_files_downloaded: - self.downloadComplete = True - else: - self.cleanUp() - self.downloadStarted = False - self.running = False - self.lock.acquire() - if not self.ctrl: - # thread will restart at this point, when the program is exiting - # so must exit if self.ctrl indicates this - - self.running = False - display_queue.close("rw") - return - self.running = True - if not createBothTempDirs(): - return - - - display_queue.put((self.parentApp.exitOnDownloadComplete, ())) - display_queue.close("rw") - - self.cleanUp() - - self.running = False - if noFiles: - self.lock.release() - - - def startStop(self): - if self.isAlive(): - if self.running: - self.running = False - else: - try: - self.lock.release() - - except thread_error: - sys.stderr.write(str(self.thread_id) + " thread error\n") - - def cleanUp(self): - """ - Deletes temporary files and folders - """ - - for tempWorkingDir in (self.videoTempWorkingDir, self.photoTempWorkingDir): - if tempWorkingDir: - # possibly delete any lingering files - if os.path.isdir(tempWorkingDir): - tf = os.listdir(tempWorkingDir) - if tf: - for f in tf: - os.remove(os.path.join(tempWorkingDir, f)) - os.rmdir(tempWorkingDir) - - def quit(self): - """ - Quits the thread - - A thread can be in one of four states: - - Not started (not alive, nothing to do) - Started and actively running (alive) - Started and paused (alive) - Completed (not alive, nothing to do) - """ - - # cleanup any temporary directories and files - self.cleanUp() - - if self.hasStarted: - if self.isAlive(): - self.ctrl = False - - if not self.running: - released = False - while not released: - try: - self.lock.release() - released = True - except thread_error: - sys.stderr.write("Could not release lock for thread %s\n" % self.thread_id) - - - - def on_volume_unmount(self, data1, data2): - """ needed for call to unmount volume""" - pass - - -class MediaTreeView(gtk.TreeView): - """ - TreeView display of devices and associated copying progress. - - Assumes a threaded environment. - """ - def __init__(self, parentApp): - - self.parentApp = parentApp - # device name, size of images on the device (human readable), copy progress (%), copy text - self.liststore = gtk.ListStore(str, str, float, str) - self.mapThreadToRow = {} - - 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) - - # Device refers to a thing like a camera, memory card in its reader, external hard drive, Portable Storage Device, etc. - column0 = gtk.TreeViewColumn(_("Device"), gtk.CellRendererText(), - text=0) - 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=1) - self.append_column(column1) - - column2 = gtk.TreeViewColumn(_("Download Progress"), - gtk.CellRendererProgress(), value=2, text=3) - self.append_column(column2) - self.show_all() - - def addCard(self, thread_id, cardName, sizeFiles, progress = 0.0, progressBarText = ''): - - # add the row, and get a temporary pointer to the row - iter = self.liststore.append((cardName, sizeFiles, progress, progressBarText)) - - self._setThreadMap(thread_id, iter) - - # adjust scrolled window height, based on row height and number of ready to start downloads - if workers.noReadyToStartWorkers() >= 1 or workers.noRunningWorkers() > 0: - # please note, at program startup, self.rowHeight() will be less than it will be when already running - # e.g. when starting with 3 cards, it could be 18, but when adding 2 cards to the already running program - # (with one card at startup), it could be 21 - height = (workers.noReadyToStartWorkers() + workers.noRunningWorkers() + 2) * (self.rowHeight()) - self.parentApp.media_collection_scrolledwindow.set_size_request(-1, height) - - - def updateCard(self, thread_id, totalSizeFiles): - """ - Updates the size of the photos and videos on the device, displayed to the user - """ - if thread_id in self.mapThreadToRow: - iter = self._getThreadMap(thread_id) - self.liststore.set_value(iter, 1, totalSizeFiles) - else: - sys.stderr.write("FIXME: this card is unknown") - - def removeCard(self, thread_id): - if thread_id in self.mapThreadToRow: - iter = self._getThreadMap(thread_id) - self.liststore.remove(iter) - del self.mapThreadToRow[thread_id] - - - def _setThreadMap(self, thread_id, iter): - """ - convert the temporary iter into a tree reference, which is - permanent - """ - - path = self.liststore.get_path(iter) - treerowRef = gtk.TreeRowReference(self.liststore, path) - self.mapThreadToRow[thread_id] = treerowRef - - def _getThreadMap(self, thread_id): - """ - return the tree iter for this thread - """ - - if thread_id in self.mapThreadToRow: - treerowRef = self.mapThreadToRow[thread_id] - path = treerowRef.get_path() - iter = self.liststore.get_iter(path) - return iter - else: - return None - - def updateProgress(self, thread_id, percentComplete, progressBarText, bytesDownloaded): - - iter = self._getThreadMap(thread_id) - if iter: - self.liststore.set_value(iter, 2, percentComplete) - if progressBarText: - self.liststore.set_value(iter, 3, progressBarText) - if percentComplete or bytesDownloaded: - self.parentApp.updateOverallProgress(thread_id, bytesDownloaded, percentComplete) - - - def rowHeight(self): - if not self.mapThreadToRow: - return 0 - else: - index = self.mapThreadToRow.keys()[0] - path = self.mapThreadToRow[index].get_path() - col = self.get_column(0) - return self.get_background_area(path, col)[3] + 1 - - -class ShowWarningDialog(gtk.Dialog): - """ - Displays a warning to the user that downloading directly from a - camera does not always work well - """ - def __init__(self, parent_window, postChoiceCB): - gtk.Dialog.__init__(self, _("Downloading From Cameras"), None, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_OK, gtk.RESPONSE_OK)) - - self.postChoiceCB = postChoiceCB - - primary_msg = _("Downloading directly from a camera may work poorly or not at all") - secondary_msg = _("Downloading from a card reader always works and is generally much faster. It is strongly recommended to use a card reader.") - - self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) - - primary_label = gtk.Label() - primary_label.set_markup("%s" % primary_msg) - primary_label.set_line_wrap(True) - primary_label.set_alignment(0, 0.5) - - secondary_label = gtk.Label() - secondary_label.set_text(secondary_msg) - secondary_label.set_line_wrap(True) - secondary_label.set_alignment(0, 0.5) - - self.show_again_checkbutton = gtk.CheckButton(_('_Show this message again'), True) - self.show_again_checkbutton.set_active(True) - - msg_vbox = gtk.VBox() - msg_vbox.pack_start(primary_label, False, False, padding=6) - msg_vbox.pack_start(secondary_label, False, False, padding=6) - msg_vbox.pack_start(self.show_again_checkbutton) - - icon = parent_window.render_icon(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG) - image = gtk.Image() - image.set_from_pixbuf(icon) - image.set_alignment(0, 0) - - warning_hbox = gtk.HBox() - warning_hbox.pack_start(image, False, False, padding = 12) - warning_hbox.pack_start(msg_vbox, False, False, padding = 12) - - self.vbox.pack_start(warning_hbox, padding=6) - - self.set_border_width(6) - self.set_has_separator(False) - - self.set_default_response(gtk.RESPONSE_OK) - - self.set_transient_for(parent_window) - self.show_all() - - self.connect('response', self.on_response) - - def on_response(self, device_dialog, response): - show_again = self.show_again_checkbutton.get_active() - self.postChoiceCB(self, show_again) - -class UseDeviceDialog(gtk.Dialog): - def __init__(self, parent_window, path, volume, autostart, postChoiceCB): - gtk.Dialog.__init__(self, _('Device Detected'), None, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_NO, gtk.RESPONSE_CANCEL, - gtk.STOCK_YES, gtk.RESPONSE_OK)) - - self.postChoiceCB = postChoiceCB - - self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt - prompt_label = gtk.Label(_('Should this device or partition be used to download photos or videos from?')) - prompt_label.set_line_wrap(True) - prompt_hbox = gtk.HBox() - prompt_hbox.pack_start(prompt_label, False, False, padding=6) - device_label = gtk.Label() - device_label.set_markup("%s" % volume.get_name(limit=0)) - device_hbox = gtk.HBox() - device_hbox.pack_start(device_label, False, False) - path_label = gtk.Label() - path_label.set_markup("%s" % path) - path_hbox = gtk.HBox() - path_hbox.pack_start(path_label, False, False) - - icon = volume.get_icon_pixbuf(36) - if icon: - image = gtk.Image() - image.set_from_pixbuf(icon) - - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt - self.always_checkbutton = gtk.CheckButton(_('_Remember this choice'), True) - - if icon: - device_hbox_icon = gtk.HBox(homogeneous=False, spacing=6) - device_hbox_icon.pack_start(image, False, False, padding = 6) - device_vbox = gtk.VBox(homogeneous=True, spacing=6) - device_vbox.pack_start(device_hbox, False, False) - device_vbox.pack_start(path_hbox, False, False) - device_hbox_icon.pack_start(device_vbox, False, False) - self.vbox.pack_start(device_hbox_icon, padding = 6) - else: - self.vbox.pack_start(device_hbox, padding=6) - self.vbox.pack_start(path_hbox, padding = 6) - - self.vbox.pack_start(prompt_hbox, padding=6) - self.vbox.pack_start(self.always_checkbutton, padding=6) - - self.set_border_width(6) - self.set_has_separator(False) - - self.set_default_response(gtk.RESPONSE_OK) - - - self.set_transient_for(parent_window) - self.show_all() - self.path = path - self.volume = volume - self.autostart = autostart - - self.connect('response', self.on_response) - - def on_response(self, device_dialog, response): - userSelected = False - permanent_choice = self.always_checkbutton.get_active() - if response == gtk.RESPONSE_OK: - userSelected = True - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt - cmd_line(_("%s selected for downloading from" % self.volume.get_name(limit=0))) - if permanent_choice: - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt - cmd_line(_("This device or partition will always be used to download from")) - else: - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt - cmd_line(_("%s rejected as a download device" % self.volume.get_name(limit=0))) - if permanent_choice: - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt - cmd_line(_("This device or partition will never be used to download from")) - - self.postChoiceCB(self, userSelected, permanent_choice, self.path, - self.volume, self.autostart) - -class RemoveAllJobCodeDialog(gtk.Dialog): - def __init__(self, parent_window, postChoiceCB): - gtk.Dialog.__init__(self, _('Remove all Job Codes?'), None, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_NO, gtk.RESPONSE_CANCEL, - gtk.STOCK_YES, gtk.RESPONSE_OK)) - - self.postChoiceCB = postChoiceCB - self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) - - prompt_hbox = gtk.HBox() - - icontheme = gtk.icon_theme_get_default() - icon = icontheme.load_icon('gtk-dialog-question', 36, gtk.ICON_LOOKUP_USE_BUILTIN) - if icon: - image = gtk.Image() - image.set_from_pixbuf(icon) - prompt_hbox.pack_start(image, False, False, padding = 6) - - prompt_label = gtk.Label(_('Should all Job Codes be removed?')) - prompt_label.set_line_wrap(True) - prompt_hbox.pack_start(prompt_label, False, False, padding=6) - - self.vbox.pack_start(prompt_hbox, padding=6) - - self.set_border_width(6) - self.set_has_separator(False) - - self.set_default_response(gtk.RESPONSE_OK) - - - self.set_transient_for(parent_window) - self.show_all() - - - self.connect('response', self.on_response) - - def on_response(self, device_dialog, response): - userSelected = response == gtk.RESPONSE_OK - self.postChoiceCB(self, userSelected) - - -class JobCodeDialog(gtk.Dialog): - """ Dialog prompting for a job code""" - - def __init__(self, parent_window, job_codes, default_job_code, postJobCodeEntryCB, autoStart, downloadSelected, entryOnly): - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode - gtk.Dialog.__init__(self, _('Enter a Job Code'), None, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, - gtk.STOCK_OK, gtk.RESPONSE_OK)) - - - self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader.svg')) - self.postJobCodeEntryCB = postJobCodeEntryCB - self.autoStart = autoStart - self.downloadSelected = downloadSelected - - self.combobox = gtk.combo_box_entry_new_text() - for text in job_codes: - self.combobox.append_text(text) - - self.job_code_hbox = gtk.HBox(homogeneous = False) - - if len(job_codes) and not entryOnly: - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode - task_label = gtk.Label(_('Enter a new Job Code, or select a previous one')) - else: - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode - task_label = gtk.Label(_('Enter a new Job Code')) - task_label.set_line_wrap(True) - task_hbox = gtk.HBox() - task_hbox.pack_start(task_label, False, False, padding=6) - - label = gtk.Label(_('Job Code:')) - self.job_code_hbox.pack_start(label, False, False, padding=6) - self.job_code_hbox.pack_start(self.combobox, True, True, padding=6) - - self.set_border_width(6) - self.set_has_separator(False) - - # make entry box have entry completion - self.entry = self.combobox.child - - completion = gtk.EntryCompletion() - completion.set_match_func(self.match_func) - completion.connect("match-selected", - self.on_completion_match) - completion.set_model(self.combobox.get_model()) - completion.set_text_column(0) - self.entry.set_completion(completion) - - # when user hits enter, close the dialog window - self.set_default_response(gtk.RESPONSE_OK) - self.entry.set_activates_default(True) - - if default_job_code: - self.entry.set_text(default_job_code) - - self.vbox.pack_start(task_hbox, False, False, padding = 6) - self.vbox.pack_start(self.job_code_hbox, False, False, padding=12) - - self.set_transient_for(parent_window) - self.show_all() - self.connect('response', self.on_job_code_resp) - - def match_func(self, completion, key, iter): - model = completion.get_model() - return model[iter][0].lower().startswith(self.entry.get_text().lower()) - - def on_completion_match(self, completion, model, iter): - self.entry.set_text(model[iter][0]) - self.entry.set_position(-1) - - def get_job_code(self): - return self.combobox.child.get_text() - - def on_job_code_resp(self, jc_dialog, response): - userChoseCode = False - if response == gtk.RESPONSE_OK: - userChoseCode = True - cmd_line(_("Job Code entered")) - else: - cmd_line(_("Job Code not entered")) - self.postJobCodeEntryCB(self, userChoseCode, self.get_job_code(), self.autoStart, self.downloadSelected) - - - -class SelectionTreeView(gtk.TreeView): - """ - TreeView display of photos and videos available for download - - Assumes a threaded environment. - """ - def __init__(self, parentApp): - - self.parentApp = parentApp - self.rapidApp = parentApp.parentApp - - self.liststore = gtk.ListStore( - gtk.gdk.Pixbuf, # 0 thumbnail icon - str, # 1 name (for sorting) - int, # 2 timestamp (for sorting), float converted into an int - str, # 3 date (human readable) - int, # 4 size (for sorting) - str, # 5 size (human readable) - int, # 6 isImage (for sorting) - gtk.gdk.Pixbuf, # 7 type (photo or video) - str, # 8 job code - gobject.TYPE_PYOBJECT, # 9 mediaFile (for data) - gtk.gdk.Pixbuf, # 10 status icon - int, # 11 status (downloaded, cannot download, etc, for sorting) - str, # 12 path (on the device) - str, # 13 device - int) # 14 thread id (worker the file is associated with) - - self.selected_rows = set() - - # sort by date (unless there is a problem) - self.liststore.set_sort_column_id(2, gtk.SORT_ASCENDING) - - gtk.TreeView.__init__(self, self.liststore) - - selection = self.get_selection() - selection.set_mode(gtk.SELECTION_MULTIPLE) - selection.connect('changed', self.on_selection_changed) - - self.set_rubber_banding(True) - - # Status Column - # Indicates whether file was downloaded, or a warning or error of some kind - cell = gtk.CellRendererPixbuf() - cell.set_property("yalign", 0.5) - status_column = gtk.TreeViewColumn(_("Status"), cell, pixbuf=10) - status_column.set_sort_column_id(11) - status_column.connect('clicked', self.header_clicked) - self.append_column(status_column) - - # Type of file column i.e. photo or video (displays at user request) - cell = gtk.CellRendererPixbuf() - cell.set_property("yalign", 0.5) - self.type_column = gtk.TreeViewColumn(_("Type"), cell, pixbuf=7) - self.type_column.set_sort_column_id(6) - self.type_column.set_clickable(True) - self.type_column.connect('clicked', self.header_clicked) - self.append_column(self.type_column) - self.display_type_column(self.rapidApp.prefs.display_type_column) - - #File thumbnail column - if not DOWNLOAD_VIDEO: - title = _("Photo") - else: - title = _("File") - thumbnail_column = gtk.TreeViewColumn(title) - cellpb = gtk.CellRendererPixbuf() - if not DROP_SHADOW: - cellpb.set_fixed_size(60,50) - thumbnail_column.pack_start(cellpb, False) - thumbnail_column.set_attributes(cellpb, pixbuf=0) - thumbnail_column.set_sort_column_id(1) - thumbnail_column.set_clickable(True) - thumbnail_column.connect('clicked', self.header_clicked) - self.append_column(thumbnail_column) - - # Job code column - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - self.job_code_column = gtk.TreeViewColumn(_("Job Code"), cell, text=8) - self.job_code_column.set_sort_column_id(8) - self.job_code_column.set_resizable(True) - self.job_code_column.set_clickable(True) - self.job_code_column.connect('clicked', self.header_clicked) - self.append_column(self.job_code_column) - - # Date column - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - date_column = gtk.TreeViewColumn(_("Date"), cell, text=3) - date_column.set_sort_column_id(2) - date_column.set_resizable(True) - date_column.set_clickable(True) - date_column.connect('clicked', self.header_clicked) - self.append_column(date_column) - - # Size column (displays at user request) - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - self.size_column = gtk.TreeViewColumn(_("Size"), cell, text=5) - self.size_column.set_sort_column_id(4) - self.size_column.set_resizable(True) - self.size_column.set_clickable(True) - self.size_column.connect('clicked', self.header_clicked) - self.append_column(self.size_column) - self.display_size_column(self.rapidApp.prefs.display_size_column) - - # Device column (displays at user request) - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - self.device_column = gtk.TreeViewColumn(_("Device"), cell, text=13) - self.device_column.set_sort_column_id(13) - self.device_column.set_resizable(True) - self.device_column.set_clickable(True) - self.device_column.connect('clicked', self.header_clicked) - self.append_column(self.device_column) - self.display_device_column(self.rapidApp.prefs.display_device_column) - - # Filename column (displays at user request) - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - self.filename_column = gtk.TreeViewColumn(_("Filename"), cell, text=1) - self.filename_column.set_sort_column_id(1) - self.filename_column.set_resizable(True) - self.filename_column.set_clickable(True) - self.filename_column.connect('clicked', self.header_clicked) - self.append_column(self.filename_column) - self.display_filename_column(self.rapidApp.prefs.display_filename_column) - - # Path column (displays at user request) - cell = gtk.CellRendererText() - cell.set_property("yalign", 0) - self.path_column = gtk.TreeViewColumn(_("Path"), cell, text=12) - self.path_column.set_sort_column_id(12) - self.path_column.set_resizable(True) - self.path_column.set_clickable(True) - self.path_column.connect('clicked', self.header_clicked) - self.append_column(self.path_column) - self.display_path_column(self.rapidApp.prefs.display_path_column) - - self.show_all() - - # flag used to determine if a preview should be generated or not - # there is no point generating a preview for each photo when - # select all photos is called, for instance - self.suspend_previews = False - - self.user_has_clicked_header = False + self.connect('item-activated', self.on_item_activated) + def _setup_icons(self): # icons to be displayed in status column - self.downloaded_icon = self.render_icon('rapid-photo-downloader-downloaded', gtk.ICON_SIZE_MENU) - self.download_failed_icon = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU) - self.error_icon = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU) - self.warning_icon = self.render_icon(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_MENU) - - self.download_pending_icon = self.render_icon('rapid-photo-downloader-download-pending', gtk.ICON_SIZE_MENU) - self.downloaded_with_warning_icon = self.render_icon('rapid-photo-downloader-downloaded-with-warning', gtk.ICON_SIZE_MENU) - self.downloaded_with_error_icon = self.render_icon('rapid-photo-downloader-downloaded-with-error', gtk.ICON_SIZE_MENU) + size = 16 + # standard icons + failed = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU) + self.download_failed_icon = failed.scale_simple(size, size, gtk.gdk.INTERP_HYPER) + error = self.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU) + self.error_icon = error.scale_simple(size, size, gtk.gdk.INTERP_HYPER) + warning = self.render_icon(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_MENU) + self.warning_icon = warning.scale_simple(size, size, gtk.gdk.INTERP_HYPER) + + # Rapid Photo Downloader specific icons + self.downloaded_icon = gtk.gdk.pixbuf_new_from_file_at_size( + paths.share_dir('glade3/rapid-photo-downloader-downloaded.svg'), + size, size) + self.download_pending_icon = gtk.gdk.pixbuf_new_from_file_at_size( + paths.share_dir('glade3/rapid-photo-downloader-download-pending.svg'), + 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)) - # but make it be a tick in the preview pane - self.not_downloaded_icon_tick = self.render_icon(gtk.STOCK_YES, gtk.ICON_SIZE_MENU) - - #preload generic icons - self.icon_photo = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/photo24.png')) - self.icon_video = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/video24.png')) - #with shadows - if DROP_SHADOW: - self.generic_photo_thumbnail = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/photo_small_shadow.png')) - self.generic_video_thumbnail = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/video_small_shadow.png')) - self.iconDropShadow = DropShadow(offset=(3,3), shadow = (0x34, 0x34, 0x34, 0xff), border=6) - else: - self.generic_photo_thumbnail = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/photo_small.png')) - self.generic_video_thumbnail = gtk.gdk.pixbuf_new_from_file(paths.share_dir('glade3/video_small.png')) - - self.previewed_file_treerowref = None - self.icontheme = gtk.icon_theme_get_default() - - - - def get_thread(self, iter): - """ - Returns the thread associated with the liststore's iter - """ - return self.liststore.get_value(iter, 14) - - def get_status(self, iter): - """ - Returns the status associated with the liststore's iter - """ - return self.liststore.get_value(iter, 11) - - def get_mediaFile(self, iter): - """ - Returns the mediaFile associated with the liststore's iter - """ - return self.liststore.get_value(iter, 9) - - def get_is_image(self, iter): - """ - Returns the file type (is image or video) associated with the liststore's iter - """ - return self.liststore.get_value(iter, 6) - - def get_type_icon(self, iter): - """ - Returns the file type's pixbuf associated with the liststore's iter - """ - return self.liststore.get_value(iter, 7) - - def get_job_code(self, iter): - """ - Returns the job code associated with the liststore's iter - """ - return self.liststore.get_value(iter, 8) - def get_status_icon(self, status, preview=False): + def get_status_icon(self, status): """ Returns the correct icon, based on the status """ @@ -3679,10 +502,7 @@ class SelectionTreeView(gtk.TreeView): elif status == STATUS_DOWNLOADED: status_icon = self.downloaded_icon elif status == STATUS_NOT_DOWNLOADED: - if preview: - status_icon = self.not_downloaded_icon_tick - else: - status_icon = self.not_downloaded_icon + status_icon = self.not_downloaded_icon elif status in [STATUS_DOWNLOADED_WITH_WARNING, STATUS_BACKUP_PROBLEM]: status_icon = self.downloaded_with_warning_icon elif status in [STATUS_DOWNLOAD_FAILED, STATUS_DOWNLOAD_AND_BACKUP_FAILED]: @@ -3690,1464 +510,1183 @@ class SelectionTreeView(gtk.TreeView): elif status == STATUS_DOWNLOAD_PENDING: status_icon = self.download_pending_icon else: - sys.stderr.write("FIXME: unknown status: %s\n" % status) + logger.critical("FIXME: unknown status: %s", status) status_icon = self.not_downloaded_icon - return status_icon - - def get_tree_row_refs(self): - """ - Returns a list of all tree row references - """ - tree_row_refs = [] - iter = self.liststore.get_iter_first() - while iter: - tree_row_refs.append(self.get_mediaFile(iter).treerowref) - iter = self.liststore.iter_next(iter) - return tree_row_refs - - def get_selected_tree_row_refs(self): - """ - Returns a list of tree row references for the selected rows - """ - tree_row_refs = [] - selection = self.get_selection() - model, pathlist = selection.get_selected_rows() - for path in pathlist: - iter = self.liststore.get_iter(path) - tree_row_refs.append(self.get_mediaFile(iter).treerowref) - return tree_row_refs - - def get_tree_row_iters(self, selected_only=False): - """ - Yields tree row iters + return status_icon + + def sort_by_timestamp(self): + self.liststore.set_sort_column_id(self.TIMESTAMP_COL, gtk.SORT_ASCENDING) - If selected_only is True, then only those from the selected - rows will be returned. + def on_checkbutton_toggled(self, cellrenderertoggle, path): + iter = self.liststore.get_iter(path) + self.liststore.set_value(iter, self.SELECTED_COL, not cellrenderertoggle.get_active()) + self.rapid_app.set_download_action_sensitivity() - This function is essential when modifying any content - in the list store (because rows can easily be moved when their - content changes) - """ - if selected_only: - tree_row_refs = self.get_selected_tree_row_refs() - else: - tree_row_refs = self.get_tree_row_refs() - for reference in tree_row_refs: - path = reference.get_path() - yield self.liststore.get_iter(path) + 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, mediaFile): - if debug_info: - cmd_line('Adding file %s' % mediaFile.fullFileName) - - # metadata is loaded when previews are generated before downloading - if mediaFile.metadata: - date = mediaFile.dateTime() - timestamp = mediaFile.metadata.timeStamp(missing=None) - if timestamp is None: - timestamp = mediaFile.modificationTime - # if metadata has not been loaded, substitute other values - else: - timestamp = mediaFile.modificationTime - date = datetime.datetime.fromtimestamp(timestamp) + def add_file(self, rpd_file): - timestamp = int(timestamp) - - date_human_readable = date_time_human_readable(date) - name = mediaFile.name - size = mediaFile.size - thumbnail = mediaFile.thumbnail - - if mediaFile.genericThumbnail: - if mediaFile.isImage: - thumbnail_icon = self.generic_photo_thumbnail - else: - thumbnail_icon = self.generic_video_thumbnail - else: - thumbnail_icon = common.scale2pixbuf(60, 36, thumbnail) - - if DROP_SHADOW and not mediaFile.genericThumbnail: - pil_image = pixbuf_to_image(thumbnail_icon) - pil_image = self.iconDropShadow.dropShadow(pil_image) - thumbnail_icon = image_to_pixbuf(pil_image) + thumbnail_icon = self.get_stock_icon(rpd_file.file_type) + unique_id = rpd_file.unique_id + scan_pid = rpd_file.scan_pid + timestamp = int(rpd_file.modification_time) + + iter = self.liststore.append((thumbnail_icon, + True, + unique_id, + rpd_file.display_name, + timestamp, + rpd_file.file_type, + True, + STATUS_NOT_DOWNLOADED, + self.not_downloaded_icon + )) - if mediaFile.isImage: - type_icon = self.icon_photo - else: - type_icon = self.icon_video - - status_icon = self.get_status_icon(mediaFile.status) - - if debug_info and False: - cmd_line('Thumbnail icon: %s' % thumbnail_icon) - cmd_line('Name: %s' % name) - cmd_line('Timestamp: %s' % timestamp) - cmd_line('Date: %s' % date_human_readable) - cmd_line('Size: %s %s' % (size, common.formatSizeForUser(size))) - cmd_line('Is an image: %s' % mediaFile.isImage) - cmd_line('Status: %s' % self.status_human_readable(mediaFile)) - cmd_line('Path: %s' % mediaFile.path) - cmd_line('Device name: %s' % mediaFile.deviceName) - cmd_line('Thread: %s' % mediaFile.thread_id) - cmd_line(' ') - - iter = self.liststore.append((thumbnail_icon, name, timestamp, date_human_readable, size, common.formatSizeForUser(size), mediaFile.isImage, type_icon, '', mediaFile, status_icon, mediaFile.status, mediaFile.path, mediaFile.deviceName, mediaFile.thread_id)) - - #create a reference to this row and store it in the mediaFile path = self.liststore.get_path(iter) - mediaFile.treerowref = gtk.TreeRowReference(self.liststore, path) + treerowref = gtk.TreeRowReference(self.liststore, path) - if mediaFile.status in [STATUS_CANNOT_DOWNLOAD, STATUS_WARNING]: - if not self.user_has_clicked_header: - self.liststore.set_sort_column_id(11, gtk.SORT_DESCENDING) + 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 - def no_selected_rows_available_for_download(self): - """ - Gets the number of rows the user has selected that can actually - be downloaded, and the threads they are found in - """ - v = 0 - threads = [] - model, paths = self.get_selection().get_selected_rows() - for path in paths: - iter = self.liststore.get_iter(path) - status = self.get_status(iter) - if status in [STATUS_NOT_DOWNLOADED, STATUS_WARNING]: - v += 1 - thread = self.get_thread(iter) - if thread not in threads: - threads.append(thread) - return v, threads - - def rows_available_for_download(self): - """ - Returns true if one or more rows has their status as STATUS_NOT_DOWNLOADED or STATUS_WARNING - """ - iter = self.liststore.get_iter_first() - while iter: - status = self.get_status(iter) - if status in [STATUS_NOT_DOWNLOADED, STATUS_WARNING]: - return True - iter = self.liststore.iter_next(iter) - return False + self.total_files += 1 + + 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 update_download_selected_button(self): + def on_item_activated(self, iconview, path): """ - Updates the text on the Download Selection button, and set its sensitivity """ - no_available_for_download = 0 - selection = self.get_selection() - model, paths = selection.get_selected_rows() - if paths: - path = paths[0] + iter = self.liststore.get_iter(path) + self.show_preview(iter=iter) + self.advance_get_preview_image(iter) + + + def _get_preview(self, unique_id, rpd_file): + if unique_id not in self.previews_being_fetched: + #check if preview should be from a downloaded file, or the source + if rpd_file.status in DOWNLOADED: + file_location = rpd_file.download_full_file_name + else: + file_location = rpd_file.full_file_name + self.preview_manager.get_preview(unique_id, file_location, + 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) + elif iter is not None: + unique_id = self.get_unique_id_from_iter(iter) + else: + # neither an iter or a unique_id were passed + # use iter from first selected file + # if none is selected, choose the first file + selected = self.get_selected_items() + if selected: + path = selected[0] + else: + path = 0 iter = self.liststore.get_iter(path) + unique_id = self.get_unique_id_from_iter(iter) - #update button text - no_available_for_download, threads = self.no_selected_rows_available_for_download() - if no_available_for_download and workers.scanComplete(threads): - self.rapidApp.download_selected_button.set_label(self.rapidApp.DOWNLOAD_SELECTED_LABEL + " (%s)" % no_available_for_download) - self.rapidApp.download_selected_button.set_sensitive(True) - else: - #nothing was selected, or nothing is available from what the user selected, or should not download right now - self.rapidApp.download_selected_button.set_label(self.rapidApp.DOWNLOAD_SELECTED_LABEL) - self.rapidApp.download_selected_button.set_sensitive(False) - - def on_selection_changed(self, selection): - """ - Update download selected button and preview the most recently - selected row in the treeview - """ - self.update_download_selected_button() - size = selection.count_selected_rows() - if size == 0: - self.selected_rows = set() - self.show_preview(None) + rpd_file = self.rpd_files[unique_id] + + if unique_id in self.previews: + preview_image = self.previews[unique_id] else: - if size <= len(self.selected_rows): - # discard everything, start over - self.selected_rows = set() - self.selection_size = size - model, paths = selection.get_selected_rows() - for path in paths: - iter = self.liststore.get_iter(path) - ref = self.get_mediaFile(iter).treerowref - - if ref not in self.selected_rows: - self.show_preview(iter) - self.selected_rows.add(ref) + # request daemon process to get a full size thumbnail + self._get_preview(unique_id, rpd_file) + if unique_id in self.thumbnails: + preview_image = self.thumbnails[unique_id] + else: + preview_image = self.get_stock_icon(rpd_file.file_type) + + checked = self.liststore.get_value(iter, self.SELECTED_COL) + include_checkbutton_visible = rpd_file.status == STATUS_NOT_DOWNLOADED + self.rapid_app.show_preview_image(unique_id, preview_image, + include_checkbutton_visible, checked) - def clear_all(self, thread_id = None): - if thread_id is None: - self.liststore.clear() - self.show_preview(None) - else: + def _get_next_iter(self, iter): + iter = self.liststore.iter_next(iter) + if iter is None: iter = self.liststore.get_iter_first() - while iter: - t = self.get_thread(iter) - if t == thread_id: - if self.previewed_file_treerowref: - mediaFile = self.get_mediaFile(iter) - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(None) - self.liststore.remove(iter) - # need to start over, or else bad things happen - iter = self.liststore.get_iter_first() - else: - iter = self.liststore.iter_next(iter) - - def refreshSampleDownloadFolders(self, thread_id = None): - """ - Refreshes the download folder of every file that has not yet been downloaded - - This is useful when the user updates the preferences, and the scan has already occurred (or is occurring) - - If thread_id is specified, will only update rows with that thread - """ - for iter in self.get_tree_row_iters(): - status = self.get_status(iter) - if status in [STATUS_NOT_DOWNLOADED, STATUS_WARNING, STATUS_CANNOT_DOWNLOAD]: - regenerate = True - if thread_id is not None: - t = self.get_thread(iter) - regenerate = t == thread_id - - if regenerate: - mediaFile = self.get_mediaFile(iter) - if mediaFile.isImage: - mediaFile.downloadFolder = self.rapidApp.prefs.download_folder - else: - mediaFile.downloadFolder = self.rapidApp.prefs.video_download_folder - mediaFile.samplePath = os.path.join(mediaFile.downloadFolder, mediaFile.sampleSubfolder) - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(iter) - - def _refreshNameFactories(self): - sample_download_start_time = datetime.datetime.now() - self.imageRenamePrefsFactory = rn.ImageRenamePreferences(self.rapidApp.prefs.image_rename, self, - self.rapidApp.fileSequenceLock, sequences) - self.imageRenamePrefsFactory.setDownloadStartTime(sample_download_start_time) - self.videoRenamePrefsFactory = rn.VideoRenamePreferences(self.rapidApp.prefs.video_rename, self, - self.rapidApp.fileSequenceLock, sequences) - self.videoRenamePrefsFactory.setDownloadStartTime(sample_download_start_time) - self.subfolderPrefsFactory = rn.SubfolderPreferences(self.rapidApp.prefs.subfolder, self) - self.subfolderPrefsFactory.setDownloadStartTime(sample_download_start_time) - self.videoSubfolderPrefsFactory = rn.VideoSubfolderPreferences(self.rapidApp.prefs.video_subfolder, self) - self.videoSubfolderPrefsFactory.setDownloadStartTime(sample_download_start_time) - self.strip_characters = self.rapidApp.prefs.strip_characters - - - def refreshGeneratedSampleSubfolderAndName(self, thread_id = None): - """ - Refreshes the name, subfolder and status of every file that has not yet been downloaded + return iter - This is useful when the user updates the preferences, and the scan has already occurred (or is occurring) - - If thread_id is specified, will only update rows with that thread - """ - self._setUsesJobCode() - self._refreshNameFactories() - for iter in self.get_tree_row_iters(): - status = self.get_status(iter) - if status in [STATUS_NOT_DOWNLOADED, STATUS_WARNING, STATUS_CANNOT_DOWNLOAD]: - regenerate = True - if thread_id is not None: - t = self.get_thread(iter) - regenerate = t == thread_id - - if regenerate: - mediaFile = self.get_mediaFile(iter) - self.generateSampleSubfolderAndName(mediaFile, iter) - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(iter) - - def generateSampleSubfolderAndName(self, mediaFile, iter): - problem = pn.Problem() - if mediaFile.isImage: - fallback_date = None - subfolderPrefsFactory = self.subfolderPrefsFactory - renamePrefsFactory = self.imageRenamePrefsFactory - nameUsesJobCode = self.imageRenameUsesJobCode - subfolderUsesJobCode = self.imageSubfolderUsesJobCode + def _get_prev_iter(self, iter): + row = self.liststore.get_path(iter)[0] + if row == 0: + row = len(self.liststore)-1 else: - fallback_date = mediaFile.modificationTime - subfolderPrefsFactory = self.videoSubfolderPrefsFactory - renamePrefsFactory = self.videoRenamePrefsFactory - nameUsesJobCode = self.videoRenameUsesJobCode - subfolderUsesJobCode = self.videoSubfolderUsesJobCode - - renamePrefsFactory.setJobCode(self.get_job_code(iter)) - subfolderPrefsFactory.setJobCode(self.get_job_code(iter)) - - generateSubfolderAndName(mediaFile, problem, subfolderPrefsFactory, renamePrefsFactory, - nameUsesJobCode, subfolderUsesJobCode, - self.strip_characters, fallback_date) - if self.get_status(iter) != mediaFile.status: - self.liststore.set(iter, 11, mediaFile.status) - self.liststore.set(iter, 10, self.get_status_icon(mediaFile.status)) - mediaFile.sampleStale = False - - def _setUsesJobCode(self): - self.imageRenameUsesJobCode = rn.usesJobCode(self.rapidApp.prefs.image_rename) - self.imageSubfolderUsesJobCode = rn.usesJobCode(self.rapidApp.prefs.subfolder) - self.videoRenameUsesJobCode = rn.usesJobCode(self.rapidApp.prefs.video_rename) - self.videoSubfolderUsesJobCode = rn.usesJobCode(self.rapidApp.prefs.video_subfolder) - + row -= 1 + iter = self.liststore.get_iter(row) + return iter - def status_human_readable(self, mediaFile): - if mediaFile.status == STATUS_DOWNLOADED: - v = _('%(filetype)s was downloaded successfully') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_DOWNLOAD_FAILED: - v = _('%(filetype)s was not downloaded') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_DOWNLOADED_WITH_WARNING: - v = _('%(filetype)s was downloaded with warnings') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_BACKUP_PROBLEM: - v = _('%(filetype)s was downloaded but there were problems backing up') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_DOWNLOAD_AND_BACKUP_FAILED: - v = _('%(filetype)s was neither downloaded nor backed up') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_NOT_DOWNLOADED: - v = _('%(filetype)s is ready to be downloaded') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_DOWNLOAD_PENDING: - v = _('%(filetype)s is about to be downloaded') % {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_WARNING: - v = _('%(filetype)s will be downloaded with warnings')% {'filetype': mediaFile.displayNameCap} - elif mediaFile.status == STATUS_CANNOT_DOWNLOAD: - v = _('%(filetype)s cannot be downloaded') % {'filetype': mediaFile.displayNameCap} - return v - - def show_preview(self, iter): - - if not iter: - # clear everything except the label Preview at the top - for widget in [self.parentApp.preview_original_name_label, - self.parentApp.preview_name_label, - self.parentApp.preview_status_label, - self.parentApp.preview_problem_title_label, - self.parentApp.preview_problem_label]: - widget.set_text('') - - for widget in [self.parentApp.preview_image, - self.parentApp.preview_name_label, - self.parentApp.preview_original_name_label, - self.parentApp.preview_status_label, - self.parentApp.preview_problem_title_label, - self.parentApp.preview_problem_label - ]: - widget.set_tooltip_text('') - - self.parentApp.preview_image.clear() - self.parentApp.preview_status_icon.clear() - self.parentApp.preview_destination_expander.hide() - self.parentApp.preview_device_expander.hide() - self.previewed_file_treerowref = None - - - elif not self.suspend_previews: - mediaFile = self.get_mediaFile(iter) - - self.previewed_file_treerowref = mediaFile.treerowref + 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) - self.parentApp.set_base_preview_image(mediaFile.thumbnail) - thumbnail = self.parentApp.scaledPreviewImage() - - self.parentApp.preview_image.set_from_pixbuf(thumbnail) + # cache next image + self.advance_get_preview_image(iter, prev=False, next=True) - image_tool_tip = "%s\n%s" % (date_time_human_readable(mediaFile.dateTime(), False), common.formatSizeForUser(mediaFile.size)) - self.parentApp.preview_image.set_tooltip_text(image_tool_tip) - - if mediaFile.sampleStale and mediaFile.status in [STATUS_NOT_DOWNLOADED, STATUS_WARNING]: - self._refreshNameFactories() - self._setUsesJobCode() - self.generateSampleSubfolderAndName(mediaFile, iter) - - self.parentApp.preview_original_name_label.set_text(mediaFile.name) - self.parentApp.preview_original_name_label.set_tooltip_text(mediaFile.name) - if mediaFile.volume: - pixbuf = mediaFile.volume.get_icon_pixbuf(16) - else: - pixbuf = self.icontheme.load_icon('gtk-harddisk', 16, gtk.ICON_LOOKUP_USE_BUILTIN) - self.parentApp.preview_device_image.set_from_pixbuf(pixbuf) - self.parentApp.preview_device_label.set_text(mediaFile.deviceName) - self.parentApp.preview_device_path_label.set_text(mediaFile.path) - self.parentApp.preview_device_path_label.set_tooltip_text(mediaFile.path) + 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) - if using_gio: - folder = gio.File(mediaFile.downloadFolder) - fileInfo = folder.query_info(gio.FILE_ATTRIBUTE_STANDARD_ICON) - icon = fileInfo.get_icon() - pixbuf = common.get_icon_pixbuf(using_gio, icon, 16, fallback='folder') - else: - pixbuf = self.icontheme.load_icon('folder', 16, gtk.ICON_LOOKUP_USE_BUILTIN) - - self.parentApp.preview_destination_image.set_from_pixbuf(pixbuf) - downloadFolderName = os.path.split(mediaFile.downloadFolder)[1] - self.parentApp.preview_destination_label.set_text(downloadFolderName) + # cache next image + self.advance_get_preview_image(iter, prev=True, next=False) - if mediaFile.status in [STATUS_WARNING, STATUS_CANNOT_DOWNLOAD, STATUS_NOT_DOWNLOADED, STATUS_DOWNLOAD_PENDING]: - - self.parentApp.preview_name_label.set_text(mediaFile.sampleName) - self.parentApp.preview_name_label.set_tooltip_text(mediaFile.sampleName) - self.parentApp.preview_destination_path_label.set_text(mediaFile.samplePath) - self.parentApp.preview_destination_path_label.set_tooltip_text(mediaFile.samplePath) - else: - self.parentApp.preview_name_label.set_text(mediaFile.downloadName) - self.parentApp.preview_name_label.set_tooltip_text(mediaFile.downloadName) - self.parentApp.preview_destination_path_label.set_text(mediaFile.downloadPath) - self.parentApp.preview_destination_path_label.set_tooltip_text(mediaFile.downloadPath) - status_text = self.status_human_readable(mediaFile) - self.parentApp.preview_status_icon.set_from_pixbuf(self.get_status_icon(mediaFile.status, preview=True)) - self.parentApp.preview_status_label.set_markup('' + status_text + '') - self.parentApp.preview_status_label.set_tooltip_text(status_text) - - - if mediaFile.status in [STATUS_WARNING, STATUS_DOWNLOAD_FAILED, - STATUS_DOWNLOADED_WITH_WARNING, - STATUS_CANNOT_DOWNLOAD, - STATUS_BACKUP_PROBLEM, - STATUS_DOWNLOAD_AND_BACKUP_FAILED]: - problem_title = mediaFile.problem.get_title() - self.parentApp.preview_problem_title_label.set_markup('' + problem_title + '') - self.parentApp.preview_problem_title_label.set_tooltip_text(problem_title) - - problem_text = mediaFile.problem.get_problems() - self.parentApp.preview_problem_label.set_text(problem_text) - self.parentApp.preview_problem_label.set_tooltip_text(problem_text) - else: - self.parentApp.preview_problem_label.set_markup('') - self.parentApp.preview_problem_title_label.set_markup('') - for widget in [self.parentApp.preview_problem_title_label, - self.parentApp.preview_problem_label - ]: - widget.set_tooltip_text('') - - if self.rapidApp.prefs.display_preview_folders: - self.parentApp.preview_destination_expander.show() - self.parentApp.preview_device_expander.show() + 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)) - - def select_rows(self, range): - selection = self.get_selection() - if range == 'all': - selection.select_all() - elif range == 'none': - selection.unselect_all() - else: - # User chose to select all photos or all videos, - # or select all files with or without job codes. - - # Temporarily suspend previews while a large number of rows - # are being selected / unselected - self.suspend_previews = True + if prev: + prev_iter = self._get_prev_iter(iter) + unique_ids.append(self.get_unique_id_from_iter(prev_iter)) - iter = self.liststore.get_iter_first() - while iter is not None: - if range in ['photos', 'videos']: - type = self.get_is_image(iter) - select_row = (type and range == 'photos') or (not type and range == 'videos') - else: - job_code = self.get_job_code(iter) - select_row = (job_code and range == 'withjobcode') or (not job_code and range == 'withoutjobcode') - - if select_row: - selection.select_iter(iter) + for unique_id in unique_ids: + if not unique_id in self.previews: + rpd_file = self.rpd_files[unique_id] + self._get_preview(unique_id, rpd_file) + + def check_all(self, check_all, file_type=None): + for row in self.liststore: + if row[self.CHECKBUTTON_VISIBLE_COL]: + if file_type is not None: + if row[self.FILETYPE_COL] == file_type: + row[self.SELECTED_COL] = check_all else: - selection.unselect_iter(iter) - iter = self.liststore.iter_next(iter) + row[self.SELECTED_COL] = check_all + self.rapid_app.set_download_action_sensitivity() - self.suspend_previews = False - # select the first photo / video - iter = self.liststore.get_iter_first() - while iter is not None: - type = self.get_is_image(iter) - if (type and range == 'photos') or (not type and range == 'videos'): - self.show_preview(iter) - break - iter = self.liststore.iter_next(iter) - - - def header_clicked(self, column): - self.user_has_clicked_header = True - - def display_filename_column(self, display): + def files_are_checked_to_download(self): """ - if display is true, the column will be shown - otherwise, it will not be shown + Returns True if there is any file that the user has indicated they + intend to download, else returns False. """ - self.filename_column.set_visible(display) - - def display_size_column(self, display): - self.size_column.set_visible(display) - - def display_type_column(self, display): - if not DOWNLOAD_VIDEO: - self.type_column.set_visible(False) - else: - self.type_column.set_visible(display) - - def display_path_column(self, display): - self.path_column.set_visible(display) - - def display_device_column(self, display): - self.device_column.set_visible(display) + for row in self.liststore: + if row[self.SELECTED_COL]: + rpd_file = self.rpd_files[row[self.UNIQUE_ID_COL]] + if rpd_file.status not in DOWNLOADED: + return True + return False - def apply_job_code(self, job_code, overwrite=True, to_all_rows=False, thread_id=None): + def get_files_checked_for_download(self): """ - Applies the Job code to the selected rows, or all rows if to_all_rows is True. - - If overwrite is True, then it will overwrite any existing job code. + Returns a dict of scan ids and associated files the user has indicated + they want to download """ - - def _apply_job_code(): - status = self.get_status(iter) - if status in [STATUS_DOWNLOAD_PENDING, STATUS_WARNING, STATUS_NOT_DOWNLOADED]: - - if mediaFile.isImage: - apply = rn.usesJobCode(self.rapidApp.prefs.image_rename) or rn.usesJobCode(self.rapidApp.prefs.subfolder) - else: - apply = rn.usesJobCode(self.rapidApp.prefs.video_rename) or rn.usesJobCode(self.rapidApp.prefs.video_subfolder) - if apply: - if overwrite: - self.liststore.set(iter, 8, job_code) - mediaFile.jobcode = job_code - mediaFile.sampleStale = True + files = dict() + for row in self.liststore: + if row[self.SELECTED_COL]: + rpd_file = self.rpd_files[row[self.UNIQUE_ID_COL]] + if rpd_file.status not in DOWNLOADED: + scan_pid = rpd_file.scan_pid + if scan_pid in files: + files[scan_pid].append(rpd_file) else: - if not self.get_job_code(iter): - self.liststore.set(iter, 8, job_code) - mediaFile.jobcode = job_code - mediaFile.sampleStale = True - else: - pass - #if they got an existing job code, may as well keep it there in case the user - #reactivates job codes again in their prefs - - if to_all_rows or thread_id is not None: - for iter in self.get_tree_row_iters(): - apply = True - if thread_id is not None: - t = self.get_thread(iter) - apply = t == thread_id + files[scan_pid] = [rpd_file,] - if apply: - mediaFile = self.get_mediaFile(iter) - _apply_job_code() - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(iter) - else: - for iter in self.get_tree_row_iters(selected_only = True): - mediaFile = self.get_mediaFile(iter) - _apply_job_code() - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(iter) - - def job_code_missing(self, selected_only): + return files + + + def mark_download_pending(self, files_by_scan_pid): """ - Returns True if any of the pending downloads do not have a - job code assigned. - - If selected_only is True, will only check in rows that the - user has selected. + Sets status to download pending and updates thumbnails display """ + for scan_pid in files_by_scan_pid: + for rpd_file in files_by_scan_pid[scan_pid]: + unique_id = rpd_file.unique_id + self.rpd_files[unique_id].status = STATUS_DOWNLOAD_PENDING + iter = self.get_iter_from_unique_id(unique_id) + self.liststore.set_value(iter, self.CHECKBUTTON_VISIBLE_COL, False) + self.liststore.set_value(iter, self.SELECTED_COL, False) + self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, STATUS_DOWNLOAD_PENDING) + icon = self.get_status_icon(STATUS_DOWNLOAD_PENDING) + self.liststore.set_value(iter, self.STATUS_ICON_COL, icon) + + def select_image(self, unique_id): + iter = self.get_iter_from_unique_id(unique_id) + path = self.liststore.get_path(iter) + self.select_path(path) + self.scroll_to_path(path, use_align=False, row_align=0.5, col_align=0.5) - def _job_code_missing(iter): - status = self.get_status(iter) - if status in [STATUS_WARNING, STATUS_NOT_DOWNLOADED]: - is_image = self.get_is_image(iter) - job_code = self.get_job_code(iter) - return needAJobCode.needAJobCode(job_code, is_image) - return False - - self._setUsesJobCode() - needAJobCode = NeedAJobCode(self.rapidApp.prefs) - - v = False - if selected_only: - selection = self.get_selection() - model, pathlist = selection.get_selected_rows() - for path in pathlist: - iter = self.liststore.get_iter(path) - v = _job_code_missing(iter) - if v: - break + def get_stock_icon(self, file_type): + if file_type == rpdfile.FILE_TYPE_PHOTO: + return self.stock_photo_thumbnails.stock_thumbnail_image_icon else: - iter = self.liststore.get_iter_first() - while iter: - v = _job_code_missing(iter) - if v: - break - iter = self.liststore.iter_next(iter) - return v - + return self.stock_video_thumbnails.stock_thumbnail_image_icon + + def update_status_post_download(self, rpd_file): + iter = self.get_iter_from_unique_id(rpd_file.unique_id) + self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, rpd_file.status) + icon = self.get_status_icon(rpd_file.status) + self.liststore.set_value(iter, self.STATUS_ICON_COL, icon) + self.rpd_files[rpd_file.unique_id] = rpd_file + + def generate_thumbnails(self, scan_pid): + """Initiate thumbnail generation for files scanned in one process + """ + rpd_files = [self.rpd_files[unique_id] for unique_id in self.process_index[scan_pid]] + self.thumbnail_manager.add_task(rpd_files) - def _set_download_pending(self, iter, threads): - existing_status = self.get_status(iter) - if existing_status in [STATUS_WARNING, STATUS_NOT_DOWNLOADED]: - self.liststore.set(iter, 11, STATUS_DOWNLOAD_PENDING) - self.liststore.set(iter, 10, self.download_pending_icon) - # this value is in a thread's list of files to download - mediaFile = self.get_mediaFile(iter) - # each thread will see this change in status - mediaFile.status = STATUS_DOWNLOAD_PENDING - thread = self.get_thread(iter) - if thread not in threads: - threads.append(thread) - - def set_status_to_download_pending(self, selected_only, thread_id=None): + def update_thumbnail(self, thumbnail_data): """ - Sets status of files to be download pending, if they are waiting to be downloaded - if selected_only is true, only applies to selected rows + Takes the generated thumbnail and updates the display - If thread_id is not None, then after the statuses have been set, - the thread will be restarted (this is intended for the cases - where this method is called from a thread and auto start is True) - - Returns a list of threads which can be downloaded + If the thumbnail_data includes a second image, that is used to + update the thumbnail list using the unique_id """ - threads = [] + unique_id = thumbnail_data[0] + thumbnail_icon = thumbnail_data[1] - if selected_only: - for iter in self.get_tree_row_iters(selected_only = True): - self._set_download_pending(iter, threads) - else: - for iter in self.get_tree_row_iters(): - apply = True - if thread_id is not None: - t = self.get_thread(iter) - apply = t == thread_id - if apply: - self._set_download_pending(iter, threads) - - if thread_id is not None: - # restart the thread - workers[thread_id].startStop() - return threads - - def update_status_post_download(self, treerowref): - path = treerowref.get_path() - if not path: - sys.stderr.write("FIXME: SelectionTreeView treerowref no longer refers to valid row\n") - else: + if thumbnail_icon is not None: + # get the thumbnail icon in PIL format + thumbnail_icon = thumbnail_icon.get_image() + + treerowref = self.treerow_index[unique_id] + path = treerowref.get_path() iter = self.liststore.get_iter(path) - mediaFile = self.get_mediaFile(iter) - status = mediaFile.status - self.liststore.set(iter, 11, status) - self.liststore.set(iter, 10, self.get_status_icon(status)) - # If this row is currently previewed, then should update the preview - if mediaFile.treerowref == self.previewed_file_treerowref: - self.show_preview(iter) - - -class SelectionVBox(gtk.VBox): - """ - Dialog from which the user can select photos and videos to download - """ + if thumbnail_icon: + self.liststore.set(iter, 0, thumbnail_icon) + + if len(thumbnail_data) > 2: + # get the 2nd image in PIL format + self.thumbnails[unique_id] = thumbnail_data[2].get_image() - - def __init__(self, parentApp): - """ - Initialize values for log dialog, but do not display. - """ + + def thumbnail_results(self, source, condition): + connection = self.thumbnail_manager.get_pipe(source) - gtk.VBox.__init__(self) - self.parentApp = parentApp + conn_type, data = connection.recv() - tiny_screen = TINY_SCREEN - if tiny_screen: - config.max_thumbnail_size = 160 + if conn_type == rpdmp.CONN_COMPLETE: + 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_files: + self.rapid_app.download_progressbar.set_fraction(0.0) + self.rapid_app.download_progressbar.set_text('') + else: + self.rapid_app.download_progressbar.set_fraction( + float(self.thumbnails_generated) / self.total_files) + - selection_scrolledwindow = gtk.ScrolledWindow() - selection_scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - selection_viewport = gtk.Viewport() + return True + def preview_results(self, unique_id, preview_full_size, preview_small): + """ + Receive a full size preview image and update + """ + self.previews_being_fetched.remove(unique_id) + if preview_full_size: + preview_image = preview_full_size.get_image() + self.previews[unique_id] = preview_image + self.rapid_app.update_preview_image(unique_id, preview_image) + + + def clear_all(self, scan_pid=None, keep_downloaded_files=False): + """ + Removes files from display and internal tracking. - self.selection_treeview = SelectionTreeView(self) + If scan_pid is not None, then only files matching that scan_pid will + be removed. Otherwise, everything will be removed. - selection_scrolledwindow.add(self.selection_treeview) - + 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: + self.liststore.clear() + self.treerow_index = {} + self.process_index = {} + + self.rpd_files = {} + else: + if scan_pid in self.process_index: + for unique_id in self.process_index[scan_pid]: + rpd_file = self.rpd_files[unique_id] + if not keep_downloaded_files or not rpd_file.status in DOWNLOADED: + treerowref = self.treerow_index[rpd_file.unique_id] + path = treerowref.get_path() + iter = self.liststore.get_iter(path) + self.liststore.remove(iter) + del self.treerow_index[rpd_file.unique_id] + del self.rpd_files[rpd_file.unique_id] + if not keep_downloaded_files or not len(self.process_index[scan_pid]): + del self.process_index[scan_pid] + +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 + + + def add_task(self, task): + pid = self._setup_task(task) + return pid - # Job code controls - self.add_job_code_combo() - left_pane_vbox = gtk.VBox(spacing = 12) - left_pane_vbox.pack_start(selection_scrolledwindow, True, True) - left_pane_vbox.pack_start(self.job_code_hbox, False, True) - - # Window sizes - #selection_scrolledwindow.set_size_request(350, -1) + def _setup_task(self, task): + task_results_conn, task_process_conn = Pipe(duplex=False) - # Preview pane + source = task_results_conn.fileno() + self._pipes[source] = task_results_conn + gobject.io_add_watch(source, gobject.IO_IN, self.results_callback) - # Zoom in and out slider (make the image bigger / smaller) + terminate_queue = Queue() + run_event = Event() + run_event.set() - # Zoom out (on the left of the slider) - self.zoom_out_eventbox = gtk.EventBox() - self.zoom_out_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK) - self.zoom_out_image = gtk.Image() - self.zoom_out_image.set_from_file(paths.share_dir('glade3/zoom-out.png')) - self.zoom_out_eventbox.add(self.zoom_out_image) - self.zoom_out_eventbox.connect("button_press_event", self.zoom_out_0_callback) + return self._initiate_task(task, task_process_conn, terminate_queue, run_event) - # Zoom in (on the right of the slider) - self.zoom_in_eventbox = gtk.EventBox() - self.zoom_in_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK) - self.zoom_in_image = gtk.Image() - self.zoom_in_image.set_from_file(paths.share_dir('glade3/zoom-in.png')) - self.zoom_in_eventbox.add(self.zoom_in_image) - self.zoom_in_eventbox.connect("button_press_event", self.zoom_in_100_callback) + def _initiate_task(self, task, task_process_conn, terminate_queue, run_event): + logger.error("Implement child class method!") - self.slider_adjustment = gtk.Adjustment(value=self.parentApp.prefs.preview_zoom, - lower=config.MIN_THUMBNAIL_SIZE, upper=config.max_thumbnail_size, - step_incr=1.0, page_incr=config.THUMBNAIL_INCREMENT, page_size=0) - self.slider_adjustment.connect("value_changed", self.resize_image_callback) - self.slider_hscale = gtk.HScale(self.slider_adjustment) - self.slider_hscale.set_draw_value(False) # don't display numeric value - self.slider_hscale.set_size_request(config.MIN_THUMBNAIL_SIZE * 2, -1) + + 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 request_termination(self): + """ + Send a signal to processes that they should immediately terminate + """ + requested = False + for p in self.processes(): + if p[0].is_alive(): + requested = True + p[1].put(None) + # The process might be paused: let it run + run_event = p[2] + if not run_event.is_set(): + run_event.set() + + return requested + + def terminate_forcefully(self): + """ + Forcefully terminates any running processes. Use with great caution. + No cleanup action is performed. + As python essential reference (4th edition) says, if the process + 'holds a lock or is involved with interprocess communication, + terminating it might cause a deadlock or corrupted I/O.' + """ - #Preview image - self.base_preview_image = None # large size image used to scale down from - self.preview_image = gtk.Image() + for p in self.processes(): + if p[0].is_alive(): + p[0].terminate() - self.preview_image.set_alignment(0, 0.5) - #leave room for thumbnail shadow - if DROP_SHADOW: - self.cacheDropShadow() - else: - self.shadow_size = 0 + + def get_pipe(self, source): + return self._pipes[source] - image_size, shadow_size, offset = self._imageAndShadowSize() + def get_no_active_processes(self): + """ + Returns how many processes are currently active, i.e. running + """ + i = 0 + for p in self.processes(): + if p[0].is_alive(): + i += 1 + return i + + +class ScanManager(TaskManager): + + def __init__(self, results_callback, batch_size, generate_folder, + add_device_function): + TaskManager.__init__(self, results_callback, batch_size) + self.add_device_function = add_device_function + self.generate_folder = generate_folder - self.preview_image.set_size_request(image_size, image_size) + def _initiate_task(self, device, task_process_conn, terminate_queue, run_event): + scan = scan_process.Scan(device.get_path(), self.batch_size, self.generate_folder, + task_process_conn, terminate_queue, run_event) + scan.start() + self._processes.append((scan, terminate_queue, run_event)) + self.add_device_function(scan.pid, device, + # This refers to when a device like a hard drive is having its contents scanned, + # looking for photos or videos. It is visible initially in the progress bar for each device + # (which normally holds "x photos and videos"). + # It maybe displayed only briefly if the contents of the device being scanned is small. + progress_bar_text=_('scanning...')) + + return scan.pid + +class CopyFilesManager(TaskManager): + + def _initiate_task(self, task, task_process_conn, terminate_queue, run_event): + photo_download_folder = task[0] + video_download_folder = task[1] + scan_pid = task[2] + files = task[3] - #labels to display file information + copy_files = copyfiles.CopyFiles(photo_download_folder, + video_download_folder, + files, 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 - #Original filename - self.preview_original_name_label = gtk.Label() - self.preview_original_name_label.set_alignment(0, 0.5) - self.preview_original_name_label.set_ellipsize(pango.ELLIPSIZE_END) +class ThumbnailManager(TaskManager): + def _initiate_task(self, files, task_process_conn, terminate_queue, run_event): + generator = tn.GenerateThumbnails(files, self.batch_size, task_process_conn, terminate_queue, run_event) + generator.start() + self._processes.append((generator, terminate_queue, run_event)) + return generator.pid + + +class SingleInstanceTaskManager: + """ + Base class to manage single instance processes. Examples are daemon + processes, but also a non-daemon process that has one simple task. + + Core (infrastructure) functionality is implemented in this class. + Derived classes should implemented functionality to actually implement + specific tasks. + """ + def __init__(self, results_callback): + self.results_callback = results_callback - #Device (where it will be downloaded from) - self.preview_device_expander = gtk.Expander() - self.preview_device_label = gtk.Label() - self.preview_device_label.set_alignment(0, 0.5) - self.preview_device_image = gtk.Image() + self.task_results_conn, self.task_process_conn = Pipe(duplex=True) - self.preview_device_path_label = gtk.Label() - self.preview_device_path_label.set_alignment(0, 0.5) - self.preview_device_path_label.set_ellipsize(pango.ELLIPSIZE_MIDDLE) - self.preview_device_path_label.set_padding(30, 0) - self.preview_device_expander.add(self.preview_device_path_label) + source = self.task_results_conn.fileno() + gobject.io_add_watch(source, gobject.IO_IN, self.task_results) + - device_hbox = gtk.HBox(False, spacing = 6) - device_hbox.pack_start(self.preview_device_image) - device_hbox.pack_start(self.preview_device_label, True, True) +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() - self.preview_device_expander.set_label_widget(device_hbox) + def get_preview(self, unique_id, full_file_name, file_type, size_max): + self.task_results_conn.send((unique_id, full_file_name, file_type, size_max)) - #Filename that has been generated - self.preview_name_label = gtk.Label() - self.preview_name_label.set_alignment(0, 0.5) - self.preview_name_label.set_ellipsize(pango.ELLIPSIZE_END) + 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 - #Download destination - self.preview_destination_expander = gtk.Expander() - self.preview_destination_label = gtk.Label() - self.preview_destination_label.set_alignment(0, 0.5) - self.preview_destination_image = gtk.Image() +class SubfolderFileManager(SingleInstanceTaskManager): + """ + Manages the daemon process that renames files and creates subfolders + """ + def __init__(self, results_callback, sequence_values): + SingleInstanceTaskManager.__init__(self, results_callback) + self._subfolder_file = subfolderfile.SubfolderFile(self.task_process_conn, sequence_values) + self._subfolder_file.start() + + def rename_file_and_move_to_subfolder(self, download_succeeded, + download_count, rpd_file): + + 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) + return True - self.preview_destination_path_label = gtk.Label() - self.preview_destination_path_label.set_alignment(0, 0.5) - self.preview_destination_path_label.set_ellipsize(pango.ELLIPSIZE_MIDDLE) - self.preview_destination_path_label.set_padding(30, 0) - self.preview_destination_expander.add(self.preview_destination_path_label) - destination_hbox = gtk.HBox(False, spacing = 6) - destination_hbox.pack_start(self.preview_destination_image) - destination_hbox.pack_start(self.preview_destination_label, True, True) - - self.preview_destination_expander.set_label_widget(destination_hbox) +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) - #Status of the file + def set_image(self, image): + self.base_image = image - self.preview_status_icon = gtk.Image() - self.preview_status_icon.set_size_request(16,16) - - self.preview_status_label = gtk.Label() - self.preview_status_label.set_alignment(0, 0.5) - self.preview_status_label.set_ellipsize(pango.ELLIPSIZE_END) - self.preview_status_label.set_padding(12, 0) - - #Title of problems encountered in generating the name / subfolder - self.preview_problem_title_label = gtk.Label() - self.preview_problem_title_label.set_alignment(0, 0.5) - self.preview_problem_title_label.set_ellipsize(pango.ELLIPSIZE_END) - self.preview_problem_title_label.set_padding(12, 0) + #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 - #Details of what the problem(s) are - self.preview_problem_label = gtk.Label() - self.preview_problem_label.set_alignment(0, 0) - self.preview_problem_label.set_line_wrap(True) - self.preview_problem_label.set_padding(12, 0) - #Can't combine wrapping and ellipsize, sadly - #self.preview_problem_label.set_ellipsize(pango.ELLIPSIZE_END) - - #Put content into table - # Use a table so we can do the Gnome HIG layout more easily - self.preview_table = gtk.Table(10, 4) - self.preview_table.set_row_spacings(12) - left_spacer = gtk.Label('') - left_spacer.set_padding(12, 0) - right_spacer = gtk.Label('') - right_spacer.set_padding(6, 0) - - - spacer2 = gtk.Label('') - - #left and right spacers - self.preview_table.attach(left_spacer, 0, 1, 1, 2, xoptions=gtk.SHRINK, yoptions=gtk.SHRINK) - self.preview_table.attach(right_spacer, 3, 4, 1, 2, xoptions=gtk.SHRINK, yoptions=gtk.SHRINK) - - row = 0 - zoom_hbox = gtk.HBox() - zoom_hbox.pack_start(self.zoom_out_eventbox, False, False) - zoom_hbox.pack_start(self.slider_hscale, False, False) - zoom_hbox.pack_start(self.zoom_in_eventbox, False, False) - - self.preview_table.attach(zoom_hbox, 1, 3, row, row+1, yoptions=gtk.SHRINK) - - row += 1 - self.preview_table.attach(self.preview_image, 1, 3, row, row+1, yoptions=gtk.SHRINK) - row += 1 - - self.preview_table.attach(self.preview_original_name_label, 1, 3, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.SHRINK) - row += 1 - if not tiny_screen: - self.preview_table.attach(self.preview_device_expander, 1, 3, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.SHRINK) - row += 1 - - self.preview_table.attach(self.preview_name_label, 1, 3, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.SHRINK) - row += 1 - if not tiny_screen: - self.preview_table.attach(self.preview_destination_expander, 1, 3, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.SHRINK) - row += 1 - - if not tiny_screen: - self.preview_table.attach(spacer2, 0, 7, row, row+1, yoptions=gtk.SHRINK) - row += 1 - - self.preview_table.attach(self.preview_status_icon, 1, 2, row, row+1, xoptions=gtk.SHRINK, yoptions=gtk.SHRINK) - self.preview_table.attach(self.preview_status_label, 2, 3, row, row+1, yoptions=gtk.SHRINK) - row += 1 - - self.preview_table.attach(self.preview_problem_title_label, 2, 3, row, row+1, yoptions=gtk.SHRINK) - row += 1 - self.preview_table.attach(self.preview_problem_label, 2, 4, row, row+1, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.EXPAND|gtk.FILL) - row += 1 - - self.file_hpaned = gtk.HPaned() - self.file_hpaned.pack1(left_pane_vbox, shrink=False) - self.file_hpaned.pack2(self.preview_table, resize=True, shrink=False) - self.pack_start(self.file_hpaned, True, True) - if self.parentApp.prefs.hpaned_pos > 0: - self.file_hpaned.set_position(self.parentApp.prefs.hpaned_pos) - else: - # this is what the user will see the first time they run the app - self.file_hpaned.set_position(300) + self.queue_draw() + + def expose(self, widget, event): - self.show_all() - - - def set_base_preview_image(self, pixbuf): - """ - sets the unscaled pixbuf image to be displayed to the user - the actual image the user will see will depend on the scale - they've set to view it at - """ - self.base_preview_image = pixbuf + cairo_context = self.window.cairo_create() - def zoom_in(self): - self.slider_adjustment.set_value(min([config.max_thumbnail_size, int(self.slider_adjustment.get_value()) + config.THUMBNAIL_INCREMENT])) + x = event.area.x + y = event.area.y + w = event.area.width + h = event.area.height - def zoom_out(self): - self.slider_adjustment.set_value(max([config.MIN_THUMBNAIL_SIZE, int(self.slider_adjustment.get_value()) - config.THUMBNAIL_INCREMENT])) - - def zoom_in_100_callback(self, widget, value): - self.slider_adjustment.set_value(config.max_thumbnail_size) + #constrain operations to event area + cairo_context.rectangle(x, y, w, h) + cairo_context.clip_preserve() - def zoom_out_0_callback(self, widget, value): - self.slider_adjustment.set_value(config.MIN_THUMBNAIL_SIZE) - - def set_display_preview_folders(self, value): - if value and self.selection_treeview.previewed_file_treerowref: - self.preview_destination_expander.show() - self.preview_device_expander.show() + #set background color, if needed + if self.bg_color: + cairo_context.set_source_rgb(*self.bg_color) + cairo_context.fill_preserve() - else: - self.preview_destination_expander.hide() - self.preview_device_expander.hide() + if not self.base_image: + return False - def cacheDropShadow(self): - i, self.shadow_size, offset_v = self._imageAndShadowSize() - self.drop_shadow = DropShadow(offset=(offset_v,offset_v), shadow = (0x44, 0x44, 0x44, 0xff), border=self.shadow_size, trim_border = True) - - def _imageAndShadowSize(self): - image_size = int(self.slider_adjustment.get_value()) - offset_v = max([image_size / 25, 5]) # realistically size the shadow based on the size of the image - shadow_size = offset_v + 3 - image_size = image_size + offset_v * 2 + 3 - return (image_size, shadow_size, offset_v) - - def resize_image_callback(self, adjustment): - """ - Resize the preview image after the adjustment value has been - changed - """ - size = int(adjustment.value) - self.parentApp.prefs.preview_zoom = size - self.cacheDropShadow() - - pixbuf = self.scaledPreviewImage() - if pixbuf: - self.preview_image.set_from_pixbuf(pixbuf) - size = max([pixbuf.get_width(), pixbuf.get_height()]) - self.preview_image.set_size_request(size, size) - else: - self.preview_image.set_size_request(size + self.shadow_size, size + self.shadow_size) - - def scaledPreviewImage(self): - """ - Generate a scaled version of the preview image - """ - size = int(self.slider_adjustment.get_value()) - if not self.base_preview_image: - return None + frame_aspect = float(w) / h + + if frame_aspect > self.base_image_aspect: + # Frame is wider than image + height = h + width = int(height * self.base_image_aspect) else: - pixbuf = common.scale2pixbuf(size, size, self.base_preview_image) + # Frame is taller than image + width = w + height = int(width / self.base_image_aspect) - if DROP_SHADOW: - pil_image = pixbuf_to_image(pixbuf) - pil_image = self.drop_shadow.dropShadow(pil_image) - pixbuf = image_to_pixbuf(pil_image) - - return pixbuf - - def set_job_code_display(self): - """ - Shows or hides the job code entry + #resize image + pil_image = self.base_image.copy() + if self.base_image_w < width or self.base_image_h < height: + logger.debug("Upsizing image") + pil_image = tn.upsize_pil(pil_image, (width, height)) + else: + logger.debug("Downsizing image") + tn.downsize_pil(pil_image, (width, height)) + + #image width and height + image_w = pil_image.size[0] + image_h = pil_image.size[1] - If user is not using job codes in their file or subfolder names - then do not prompt for it - """ + #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() - if self.parentApp.needJobCodeForRenaming(): - self.job_code_hbox.show() - self.job_code_label.show() - self.job_code_combo.show() - self.selection_treeview.job_code_column.set_visible(True) - else: - self.job_code_hbox.hide() - self.job_code_label.hide() - self.job_code_combo.hide() - self.selection_treeview.job_code_column.set_visible(False) - - def update_job_code_combo(self): - # delete existing rows - while len(self.job_code_combo.get_model()) > 0: - self.job_code_combo.remove_text(0) - # add new ones - for text in self.parentApp.prefs.job_codes: - self.job_code_combo.append_text(text) - # clear existing entry displayed in entry box - self.job_code_entry.set_text('') + return False + + +class PreviewImage: - def add_job_code_combo(self): - self.job_code_hbox = gtk.HBox(spacing = 12) - self.job_code_hbox.set_no_show_all(True) - self.job_code_label = gtk.Label(_("Job Code:")) - - self.job_code_combo = gtk.combo_box_entry_new_text() - for text in self.parentApp.prefs.job_codes: - self.job_code_combo.append_text(text) - - # make entry box have entry completion - self.job_code_entry = self.job_code_combo.child - - self.completion = gtk.EntryCompletion() - self.completion.set_match_func(self.job_code_match_func) - self.completion.connect("match-selected", - self.on_job_code_combo_completion_match) - self.completion.set_model(self.job_code_combo.get_model()) - self.completion.set_text_column(0) - self.job_code_entry.set_completion(self.completion) - - - self.job_code_combo.connect('changed', self.on_job_code_resp) - - self.job_code_entry.connect('activate', self.on_job_code_entry_resp) - - self.job_code_combo.set_tooltip_text(_("Enter a new Job Code and press Enter, or select an existing Job Code")) - - #add widgets - self.job_code_hbox.pack_start(self.job_code_label, False, False) - self.job_code_hbox.pack_start(self.job_code_combo, True, True) - self.set_job_code_display() - - def job_code_match_func(self, completion, key, iter): - model = completion.get_model() - return model[iter][0].lower().startswith(self.job_code_entry.get_text().lower()) - - def on_job_code_combo_completion_match(self, completion, model, iter): - self.job_code_entry.set_text(model[iter][0]) - self.job_code_entry.set_position(-1) - - def on_job_code_resp(self, widget): - """ - When the user has clicked on an existing job code - """ + def __init__(self, parent_app, builder): + #set background color to equivalent of '#444444 + self.preview_image = ResizblePilImage(bg_color=(0.267, 0.267, 0.267)) + self.preview_image_eventbox = builder.get_object("preview_eventbox") + self.preview_image_eventbox.add(self.preview_image) + self.preview_image.show() + self.download_this_checkbutton = builder.get_object("download_this_checkbutton") + self.rapid_app = parent_app - # ignore changes because the user is typing in a new value - if widget.get_active() >= 0: - self.job_code_chosen(widget.get_active_text()) - - def on_job_code_entry_resp(self, widget): - """ - When the user has hit enter after entering a new job code - """ - self.job_code_chosen(widget.get_text()) + 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 job_code_chosen(self, job_code): + def set_preview_image(self, unique_id, pil_image, include_checkbutton_visible=None, + checked=None): """ - The user has selected a Job code, apply it to selected images. """ - self.selection_treeview.apply_job_code(job_code, overwrite = True) - self.completion.set_model(None) - self.parentApp.assignJobCode(job_code) - self.completion.set_model(self.job_code_combo.get_model()) - - def add_file(self, mediaFile): - self.selection_treeview.add_file(mediaFile) - + self.preview_image.set_image(pil_image) + self.unique_id = unique_id + if checked is not None: + self.download_this_checkbutton.set_active(checked) + self.download_this_checkbutton.grab_focus() + + if include_checkbutton_visible is not None: + self.download_this_checkbutton.props.visible = include_checkbutton_visible -class LogDialog(gnomeglade.Component): - """ - Displays a log of errors, warnings or other information to the user + 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. - def __init__(self, parentApp): - """ - Initialize values for log dialog, but do not display. - """ + Contains functionality for main program window, and directs all other + processes. + """ + + def __init__(self, bus, path, name, taskserver=None): + + dbus.service.Object.__init__ (self, bus, path, name) + self.running = False - gnomeglade.Component.__init__(self, - paths.share_dir(config.GLADE_FILE), - "logdialog") - + self.taskserver = taskserver - self.widget.connect("delete-event", self.hide_window) + # Setup program preferences, and set callback for when they change + self._init_prefs() - self.parentApp = parentApp - self.log_textview.set_cursor_visible(False) - self.textbuffer = self.log_textview.get_buffer() + # Initialize widgets in the main window, and variables that point to them + self._init_widgets() - self.errorTag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD, foreground="red") - self.warningTag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD) - self.resolutionTag = self.textbuffer.create_tag(style=pango.STYLE_ITALIC) + # Initialize job code handling + self._init_job_code() - def addMessage(self, thread_id, severity, problem, details, resolution): - if severity in [config.CRITICAL_ERROR, config.SERIOUS_ERROR]: - self.parentApp.error_image.show() - elif severity == config.WARNING: - self.parentApp.warning_image.show() - self.parentApp.warning_vseparator.show() + # Remember the window size from the last time the program was run, or + # set a default size + self._set_window_size() - iter = self.textbuffer.get_end_iter() - if severity in [config.CRITICAL_ERROR, config.SERIOUS_ERROR]: - self.textbuffer.insert_with_tags(iter, problem +"\n", self.errorTag) - else: - self.textbuffer.insert_with_tags(iter, problem +"\n", self.warningTag) - if details: - iter = self.textbuffer.get_end_iter() - self.textbuffer.insert(iter, details + "\n") - if resolution: - iter = self.textbuffer.get_end_iter() - self.textbuffer.insert_with_tags(iter, resolution +"\n", self.resolutionTag) + # Setup various widgets + self._setup_buttons() + self._setup_error_icons() + self._setup_icons() - iter = self.textbuffer.get_end_iter() - self.textbuffer.insert(iter, "\n") + # Show the main window + self.rapidapp.show() - # move viewport to display the latest message - adjustment = self.log_scrolledwindow.get_vadjustment() - adjustment.set_value(adjustment.upper) + # Check program preferences - don't allow auto start if there is a problem + prefs_valid = prefsrapid.check_prefs_for_validity(self.prefs) + do_not_allow_auto_start = prefs_valid + # Initialize variables with which to track important downloads results + self._init_download_tracking() - def on_logdialog_response(self, dialog, arg): - if arg == gtk.RESPONSE_CLOSE: - pass - self.parentApp.error_image.hide() - self.parentApp.warning_image.hide() - self.parentApp.warning_vseparator.hide() - self.parentApp.prefs.show_log_dialog = False - self.widget.hide() - return True - - def hide_window(self, window, event): - window.hide() - return True + # 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, + do_not_allow_auto_start=do_not_allow_auto_start) + + # Ensure the device collection scrolled window is not too small + self._set_device_collection_size() + + #~ preferencesdialog.PreferencesDialog(self) + + def on_rapidapp_destroy(self, widget, data=None): + self._terminate_processes(terminate_file_copies = True) + # save window and component sizes + self.prefs.vpaned_pos = self.main_vpaned.get_position() -class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): - def __init__(self, bus, path, name): + x, y, width, height = self.rapidapp.get_allocation() + self.prefs.main_window_size_x = width + self.prefs.main_window_size_y = height - dbus.service.Object.__init__ (self, bus, path, name) - self.running = False + self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker) - gladefile = paths.share_dir(config.GLADE_FILE) - - gnomeglade.GnomeApp.__init__(self, "rapid", __version__, gladefile, "rapidapp") - - # notifications - self.displayDownloadSummaryNotification = False - self.initPyNotify() + gtk.main_quit() - self.prefs = RapidPreferences() - self.prefs.notify_add(self.on_preference_changed) + def _terminate_processes(self, terminate_file_copies=False): - self.testing = False - if self.testing: - self.setTestingEnv() - -# sys.exit(0) + # FIXME: need more fine grained tuning here - must cancel large file + # copies midstream + logger.info("Terminating...") - # remember the window size from the last time the program was run - if self.prefs.main_window_maximized: - self.rapidapp.maximize() - 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) + scan_termination_requested = self.scan_manager.request_termination() + thumbnails_termination_requested = self.thumbnails.thumbnail_manager.request_termination() + if terminate_file_copies: + copy_files_termination_requested = self.copy_files_manager.request_termination() else: - # set a default size - self.rapidapp.set_default_size(650, 650) - - if gtk.gdk.screen_height() <= config.TINY_SCREEN_HEIGHT: - self.prefs.display_preview_folders = False - self.menu_preview_folders.set_sensitive(False) - - self.widget.show() + copy_files_termination_requested = False - self._setupIcons() + if scan_termination_requested or thumbnails_termination_requested: + time.sleep(1) + if (self.scan_manager.get_no_active_processes() > 0 or + self.thumbnails.thumbnail_manager.get_no_active_processes() > 0): + time.sleep(1) + # must try again, just in case a new scan has meanwhile started! + self.scan_manager.request_termination() + self.thumbnails.thumbnail_manager.terminate_forcefully() + self.scan_manager.terminate_forcefully() + + if terminate_file_copies and copy_files_termination_requested: + time.sleep(1) + self.copy_files_manager.terminate_forcefully() - # this must come after the window is shown - if self.prefs.vpaned_pos > 0: - self.main_vpaned.set_position(self.prefs.vpaned_pos) - else: - self.main_vpaned.set_position(66) + if terminate_file_copies: + self._clean_all_temp_dirs() - self.checkIfFirstTimeProgramEverRun() + # # # + # Events and tasks related to displaying preview images and thumbnails + # # # - displayPreferences = self.checkForUpgrade(__version__) - self.prefs.program_version = __version__ + def on_download_this_checkbutton_toggled(self, checkbutton): + value = checkbutton.get_active() + self.thumbnails.set_selected(self.preview_image.unique_id, value) + self.set_download_action_sensitivity() + + def on_preview_eventbox_button_press_event(self, widget, event): - self.timeRemaining = TimeRemaining() - self._resetDownloadInfo() - self.statusbar_context_id = self.rapid_statusbar.get_context_id("progress") + if event.type == gtk.gdk._2BUTTON_PRESS and event.button == 1: + self.show_thumbnails() + + def on_show_thumbnails_action_activate(self, action): + logger.debug("on_show_thumbnails_action_activate") + self.show_thumbnails() - # hide display of warning and error symbols in the taskbar until they are needed - self.error_image.hide() - self.warning_image.hide() - self.warning_vseparator.hide() + def on_show_image_action_activate(self, action): + logger.debug("on_show_image_action_activate") + self.thumbnails.show_preview() - if not displayPreferences: - displayPreferences = not self.checkPreferencesOnStartup() + def on_check_all_action_activate(self, action): + self.thumbnails.check_all(check_all=True) - # display download information using threads - global media_collection_treeview, log_dialog - global workers + def on_uncheck_all_action_activate(self, action): + self.thumbnails.check_all(check_all=False) - #track files that should have a suffix added to them - global duplicate_files + def on_check_all_photos_action_activate(self, action): + self.thumbnails.check_all(check_all=True, + file_type=rpdfile.FILE_TYPE_PHOTO) + + def on_check_all_videos_action_activate(self, action): + self.thumbnails.check_all(check_all=True, + file_type=rpdfile.FILE_TYPE_VIDEO) + + def on_quit_action_activate(self, action): + self.on_rapidapp_destroy(widget=self.rapidapp, data=None) - #track files that have been downloaded in this session - global downloaded_files + def on_refresh_action_activate(self, action): + self.setup_devices(on_startup=False, on_preference_change=False, + do_not_allow_auto_start=True) + + def on_get_help_action_activate(self, action): + webbrowser.open("http://www.damonlynch.net/rapid/help.html") - # control sequence numbers and letters - global sequences + 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.destroy() + + def on_report_problem_action_activate(self, action): + webbrowser.open("https://bugs.launchpad.net/rapid") + + def on_translate_action_activate(self, action): + webbrowser.open("http://www.damonlynch.net/rapid/translate.html") + + def on_donate_action_activate(self, action): + webbrowser.open("http://www.damonlynch.net/rapid/donate.html") + + def show_preview_image(self, unique_id, image, include_checkbutton_visible, checked): + if self.main_notebook.get_current_page() == 0: # thumbnails + logger.debug("Switching to preview image display") + self.main_notebook.set_current_page(1) + self.preview_image.set_preview_image(unique_id, image, include_checkbutton_visible, checked) + self.next_image_action.set_sensitive(True) + self.prev_image_action.set_sensitive(True) - # whether we need to prompt for a job code - global need_job_code_for_renaming - - duplicate_files = {} - downloaded_files = DownloadedFiles() + def update_preview_image(self, unique_id, image): + self.preview_image.update_preview_image(unique_id, image) - self.download_start_time = None + 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) - downloadsToday = self.prefs.getAndMaybeResetDownloadsToday() - sequences = rn.Sequences(downloadsToday, self.prefs.stored_sequence_no) - self.downloadStats = DownloadStats() + def on_next_image_action_activate(self, action): + if self.preview_image.unique_id is not None: + self.thumbnails.show_next_image(self.preview_image.unique_id) + + def on_prev_image_action_activate(self, action): + if self.preview_image.unique_id is not None: + self.thumbnails.show_prev_image(self.preview_image.unique_id) - # set the number of seconds gap with which to measure download time remaing - self.downloadTimeGap = 3 + def set_thumbnail_sort(self): + """ + If all the scans are complete, sets the sort order + """ + if self.scan_manager.get_no_active_processes() == 0: + self.thumbnails.sort_by_timestamp() - #locks for threadsafe file downloading and stats gathering - self.fileRenameLock = Lock() - self.fileSequenceLock = Lock() - self.statsLock = Lock() - self.downloadedFilesLock = Lock() - # log window, in dialog format - # used for displaying download information to the user + # # # + # Volume management + # # # + + def start_volume_monitor(self): + if not self.vmonitor: + self.vmonitor = gio.volume_monitor_get() + self.vmonitor.connect("mount-added", self.on_mount_added) + self.vmonitor.connect("mount-removed", self.on_mount_removed) + + + def setup_devices(self, on_startup, on_preference_change, do_not_allow_auto_start): + """ + + Setup devices from which to download from and backup to + + Sets up volumes for downloading from and backing up to + + on_startup should be True if the program is still starting, + i.e. this is being called from the program's initialization. + + on_preference_change should be True if this is being called as the + result of a preference being changed + + Removes any image media that are currently not downloaded, + or finished downloading + """ + + if self.using_volume_monitor(): + self.start_volume_monitor() + + + self.clear_non_running_downloads() + + mounts = [] + self.backup_devices = {} + + # Clear download statistics and tracking + # FIXME + + if self.using_volume_monitor(): + # either using automatically detected backup devices + # or download devices + for mount in self.vmonitor.get_mounts(): + if not mount.is_shadowed(): + path = mount.get_root().get_path() + if path: + if (path in self.prefs.device_blacklist and + self.search_for_PSD()): + logger.info("%s ignored", mount.get_name()) + else: + logger.info("Detected %s", mount.get_name()) + is_backup_mount = self.check_if_backup_mount(path) + if is_backup_mount: + self.backup_devices[path] = mount + elif (self.prefs.device_autodetection and + (dv.is_DCIM_device(path) or + self.search_for_PSD())): + mounts.append((path, mount)) + - log_dialog = LogDialog(self) + if not self.prefs.device_autodetection: + # user manually specified the path from which to download + path = self.prefs.device_location + if path: + logger.info("Using manually specified path %s", path) + if utilities.is_directory(path): + mounts.append((path, None)) + else: + logger.error("Download path does not exist: %s", path) + if self.prefs.backup_images: + if not self.prefs.backup_device_autodetection: + # user manually specified backup location + # will backup to this path, but don't need any volume info + # associated with it + self.backup_devices[self.prefs.backup_location] = None + + # Display amount of free space in a status bar message + self.display_free_space() + + if do_not_allow_auto_start: + self.auto_start_is_on = False + else: + self.auto_start_is_on = ((not on_preference_change) and + ((self.prefs.auto_download_at_startup and + on_startup) or + (self.prefs.auto_download_upon_device_insertion and + not on_startup))) + - self.volumeMonitor = None - if self.usingVolumeMonitor(): - self.startVolumeMonitor() + self.testing_auto_exit = False + self.testing_auto_exit_trip = len(mounts) + self.testing_auto_exit_trip_counter = 0 - # flag to indicate whether the user changed some preferences that - # indicate the image and backup devices should be setup again - self.rerunSetupAvailableImageAndVideoMedia = False - self.rerunSetupAvailableBackupMedia = False + + 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.scan_manager.add_task(device) + if mount is not None: + self.mounts_by_path[path] = scan_pid - # flag to indicate the user changes some preferences and the display - # of sample names and subfolders needs to be refreshed - self.refreshGeneratedSampleSubfolderAndName = False + def get_use_device(self, device): + """ Prompt user whether or not to download from this device """ - # counter to indicate how many threads need their sample names and subfolders regenerated because the user - # changes their prefs at the same time as devices were being scanned - self.noAfterScanRefreshGeneratedSampleSubfolderAndName = 0 + logger.info("Prompting whether to use %s", device.get_name()) + d = dv.UseDeviceDialog(self.rapidapp, device, self.got_use_device) - # flag to indicate the user changes some preferences and the display - # of sample download folders needs to be refreshed - self.refreshSampleDownloadFolder = False - self.noAfterScanRefreshSampleDownloadFolders = 0 + 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() - # flag to indicate that the preferences dialog window is being - # displayed to the user - self.preferencesDialogDisplayed = False + path = device.get_path() - # set up tree view display to display image devices and download status - media_collection_treeview = MediaTreeView(self) + if user_selected: + if permanent_choice and path not in self.prefs.device_whitelist: + # do NOT do a list append operation here without the assignment, + # or the actual preferences will not be updated!! + if len(self.prefs.device_whitelist): + self.prefs.device_whitelist = self.prefs.device_whitelist + [path] + else: + self.prefs.device_whitelist = [path] + scan_pid = self.scan_manager.add_task(device) + self.mounts_by_path[path] = scan_pid + + elif permanent_choice and path not in self.prefs.device_blacklist: + # do not do a list append operation here without the assignment, or the preferences will not be updated! + if len(self.prefs.device_blacklist): + self.prefs.device_blacklist = self.prefs.device_blacklist + [path] + else: + self.prefs.device_blacklist = [path] + + def search_for_PSD(self): + """ + Check to see if user preferences are to automatically search for + Portable Storage Devices or not + """ + return self.prefs.device_autodetection_psd and self.prefs.device_autodetection - self.media_collection_vbox.pack_start(media_collection_treeview) + def check_if_backup_mount(self, path): + """ + Checks to see if backups are enabled and path represents a valid backup location - #Selection display - self.selection_vbox = SelectionVBox(self) - self.selection_hbox.pack_start(self.selection_vbox, padding=12) - self.set_display_selection(self.prefs.display_selection) - self.set_display_preview_folders(self.prefs.display_preview_folders) + Checks against user preferences. + """ + identifiers = [self.prefs.backup_identifier] + if DOWNLOAD_VIDEO: + identifiers.append(self.prefs.video_backup_identifier) + if self.prefs.backup_images: + if self.prefs.backup_device_autodetection: + if dv.is_backup_media(path, identifiers): + return True + elif path == self.prefs.backup_location: + # user manually specified the path + return True + return False + + def using_volume_monitor(self): + """ + Returns True if programs needs to use gio volume monitor + """ - self.backupVolumes = {} + return (self.prefs.device_autodetection or + (self.prefs.backup_images and + self.prefs.backup_device_autodetection + )) + + def on_mount_added(self, vmonitor, mount): + """ + callback run when gio indicates a new volume + has been mounted + """ - #Help button and download buttons - self._setupDownloadbuttons() - - #status bar progress bar - self.download_progressbar = gtk.ProgressBar() - self.download_progressbar.set_size_request(150, -1) - self.download_progressbar.show() - self.download_progressbar_hbox.pack_start(self.download_progressbar, expand=False, - fill=0) - - # menus + if mount.is_shadowed(): + # ignore this type of mount + return + + path = mount.get_root().get_path() - #preview panes - self.menu_display_selection.set_active(self.prefs.display_selection) - self.menu_preview_folders.set_active(self.prefs.display_preview_folders) - - #preview columns in pane - if not DOWNLOAD_VIDEO: - self.menu_type_column.set_active(False) - self.menu_type_column.set_sensitive(False) + if path in self.prefs.device_blacklist and self.search_for_PSD(): + logger.info("Device %(device)s (%(path)s) ignored" % { + 'device': mount.get_name(), 'path': path}) else: - self.menu_type_column.set_active(self.prefs.display_type_column) - self.menu_size_column.set_active(self.prefs.display_size_column) - self.menu_filename_column.set_active(self.prefs.display_filename_column) - self.menu_device_column.set_active(self.prefs.display_device_column) - self.menu_path_column.set_active(self.prefs.display_path_column) - - self.menu_clear.set_sensitive(False) + is_backup_mount = self.check_if_backup_mount(path) + + if is_backup_mount: + if path not in self.backup_devices: + self.backup_devices[path] = mount + self.display_free_space() - need_job_code_for_renaming = self.needJobCodeForRenaming() - self.menu_select_all_without_job_code.set_sensitive(need_job_code_for_renaming) - self.menu_select_all_with_job_code.set_sensitive(need_job_code_for_renaming) - - #job code initialization - self.last_chosen_job_code = None - self.prompting_for_job_code = False - - #check to see if the download folder exists and is writable - displayPreferences_2 = not self.checkDownloadPathOnStartup() - displayPreferences = displayPreferences or displayPreferences_2 + elif self.prefs.device_autodetection and (dv.is_DCIM_device(path) or + self.search_for_PSD()): + + device = dv.Device(path=path, mount=mount) + if self.search_for_PSD() and path not in self.prefs.device_whitelist: + # prompt user if device should be used or not + self.get_use_device(device) + else: + scan_pid = self.scan_manager.add_task(device) + self.mounts_by_path[path] = scan_pid - if self.prefs.device_autodetection == False: - displayPreferences_2 = not self.checkImageDevicePathOnStartup() - displayPreferences = displayPreferences or displayPreferences_2 + def on_mount_removed(self, vmonitor, mount): + """ + callback run when gio indicates a new volume + has been mounted + """ - #setup download and backup mediums, initiating scans - self.setupAvailableImageAndBackupMedia(onStartup=True, onPreferenceChange=False, doNotAllowAutoStart = displayPreferences) + path = mount.get_root().get_path() - #adjust viewport size for displaying media - #this is important because the code in MediaTreeView.addCard() is inaccurate at program startup + # 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 media_collection_treeview.mapThreadToRow: - height = self.media_collection_viewport.size_request()[1] - self.media_collection_scrolledwindow.set_size_request(-1, height) - else: - # don't allow the media collection to be absolutely empty - self.media_collection_scrolledwindow.set_size_request(-1, 47) + 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 + + #~ if scan_pid in self.download_active_by_scan_pid: + #~ self._clean_temp_dirs_for_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() + + # 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 + """ - self.download_button.grab_default() - # for some reason, the grab focus command is not working... unsure why - self.download_button.grab_focus() + # Stop any processes currently scanning or creating thumbnails + self._terminate_processes(terminate_file_copies=False) - if displayPreferences: - PreferencesDialog(self) + # 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) + - - @dbus.service.method (config.DBUS_NAME, - in_signature='', out_signature='b') - def is_running (self): - return self.running - @dbus.service.method (config.DBUS_NAME, - in_signature='', out_signature='') - def start (self): - if self.is_running(): - self.rapidapp.present() - else: - self.running = True -# if not using_gio: - self.main() -# else: -# mainloop = gobject.MainLoop() -# mainloop.run() - self.running = False - - def setTestingEnv(self): - #self.prefs.program_version = '0.0.8~b7' - p = ['Date time', 'Image date', 'YYYYMMDD', 'Text', '-', '', 'Date time', 'Image date', 'HHMM', 'Text', '-', '', rn.SEQUENCES, rn.DOWNLOAD_SEQ_NUMBER, rn.SEQUENCE_NUMBER_3, 'Text', '-iso', '', 'Metadata', 'ISO', '', 'Text', '-f', '', 'Metadata', 'Aperture', '', 'Text', '-', '', 'Metadata', 'Focal length', '', 'Text', 'mm-', '', 'Metadata', 'Exposure time', '', 'Filename', 'Extension', 'lowercase'] - v = ['Date time', 'Video date', 'YYYYMMDD', 'Text', '-', '', 'Date time', 'Video date', 'HHMM', 'Text', '-', '', 'Sequences', 'Downloads today', 'One digit', 'Text', '-', '', 'Metadata', 'Width', '', 'Text', 'x', '', 'Metadata', 'Height', '', 'Filename', 'Extension', 'lowercase'] - f = '/home/damon/store/rapid-dump' - self.prefs.image_rename = p - self.prefs.video_rename = v - self.prefs.download_folder = f - self.prefs.video_download_folder = f - - - def _setupIcons(self): - icons = ['rapid-photo-downloader-downloaded', - 'rapid-photo-downloader-downloaded-with-error', - 'rapid-photo-downloader-downloaded-with-warning', - 'rapid-photo-downloader-download-pending', - 'rapid-photo-downloader-jobcode'] - - icon_list = [(icon, paths.share_dir('glade3/%s.svg' % icon)) for icon in icons] - common.register_iconsets(icon_list) + # # # + # Download and help buttons, and menu items + # # # - def displayFreeSpace(self): + def on_download_action_activate(self, action): """ - Displays the amount of space free on the filesystem the files will be downloaded to. - Also displays backup volumes / path being used. + Called when a download is activated """ - msg = '' - if using_gio and os.path.isdir(self.prefs.download_folder): - folder = gio.File(self.prefs.download_folder) - fileInfo = folder.query_filesystem_info(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE) - free = common.formatSizeForUser(fileInfo.get_attribute_uint64(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE)) - msg = " " + _("%(free)s available") % {'free': free} + if self.copy_files_manager.paused: + logger.debug("Download resumed") + self.resume_download() + else: + logger.debug("Download activated") - if self.prefs.backup_images: - if not self.prefs.backup_device_autodetection: - # user manually specified backup location - msg2 = _('Backing up to %(path)s') % {'path':self.prefs.backup_location} - else: - msg2 = self.displayBackupVolumes() - - if msg: - msg = _("%(freespace)s. %(backuppaths)s.") % {'freespace': msg, 'backuppaths': msg2} + if self.download_action_is_download: + self.start_download() else: - msg = msg2 - - self.rapid_statusbar.push(self.statusbar_context_id, msg) + self.pause_download() + - def checkImageDevicePathOnStartup(self): - msg = None - if not os.path.isdir(self.prefs.device_location): - msg = _("Sorry, this device location does not exist:\n%(path)s\n\nPlease resolve the problem, or modify your preferences." % {"path": self.prefs.device_location}) - - if msg: - sys.stderr.write(msg +'\n') - misc.run_dialog(_("Problem with Device Location Folder"), msg, - self, - gtk.MESSAGE_ERROR) - return False - else: - return True + def on_help_action_activate(self, action): + webbrowser.open("http://www.damonlynch.net/rapid/documentation") - def checkDownloadPathOnStartup(self): - if DOWNLOAD_VIDEO: - paths = ((self.prefs.download_folder, _('Photo')), (self.prefs.video_download_folder, _('Video'))) - else: - paths = ((self.prefs.download_folder, _('Photo')),) - msg = '' - noProblems = 0 - for path, file_type in paths: - if not os.path.isdir(path): - msg += _("The %(file_type)s Download Folder does not exist.\n") % {'file_type': file_type} - noProblems += 1 - else: - #unfortunately 'os.access(self.prefs.download_folder, os.W_OK)' is not reliable - try: - tempWorkingDir = tempfile.mkdtemp(prefix='rapid-tmp-', - dir=path) - except: - noProblems += 1 - msg += _("The %(file_type)s Download Folder exists but cannot be written to.\n") % {'file_type': file_type} - else: - os.rmdir(tempWorkingDir) + def on_preferences_action_activate(self, action): + + preferencesdialog.PreferencesDialog(self) + + def set_download_action_sensitivity(self): + """ + Sets sensitivity of Download action to enable or disable it + + Affects download button and menu item + """ + if not self.download_is_occurring(): + sensitivity = False + if self.scan_manager.get_no_active_processes() == 0: + if self.thumbnails.files_are_checked_to_download(): + sensitivity = True - if msg: - msg = _("Sorry, problems were encountered with your download folders. Please fix the problems or modify the preferences.\n\n") + msg - sys.stderr.write(msg) - if noProblems == 1: - title = _("Problem with Download Folder") - else: - title = _("Problem with Download Folders") + self.download_action.set_sensitive(sensitivity) - misc.run_dialog(title, msg, - self, - gtk.MESSAGE_ERROR) - return False + def set_download_action_label(self, is_download): + """ + Toggles label betwen pause and download + """ + + if is_download: + self.download_action.set_label(_("Download")) + self.download_action_is_download = True else: - return True + self.download_action.set_label(_("Pause")) + self.download_action_is_download = False + + # # # + # Job codes + # # # - def checkPreferencesOnStartup(self): - prefsOk = rn.checkPreferencesForValidity(self.prefs.image_rename, self.prefs.subfolder, self.prefs.video_rename, self.prefs.video_subfolder) - if not prefsOk: - msg = _("There is an error in the program preferences.") - msg += " " + _("Some preferences will be reset.") - # do not use cmd_line here, as this is a genuine error - sys.stderr.write(msg +'\n') - return prefsOk - - def needJobCodeForRenaming(self): - return rn.usesJobCode(self.prefs.image_rename) or rn.usesJobCode(self.prefs.subfolder) or rn.usesJobCode(self.prefs.video_rename) or rn.usesJobCode(self.prefs.video_subfolder) - - def assignJobCode(self, code): + + def _init_job_code(self): + self.job_code = None + self.need_job_code_for_naming = self.prefs.any_pref_uses_job_code() + + def assign_job_code(self, code): """ assign job code (which may be empty) to global variable and update user preferences Update preferences only if code is not empty. Do not duplicate job code. """ - global job_code + # FIXME + #~ global job_code if code == None: code = '' job_code = code @@ -5163,1301 +1702,841 @@ class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): jcs.remove(code) self.prefs.job_codes = [code] + jcs - - - def getShowWarningDownloadingFromCamera(self): - if self.prefs.show_warning_downloading_from_camera: - cmd_line(_("Displaying warning about downloading directly from camera")) - d = ShowWarningDialog(self.widget, self.gotShowWarningDownloadingFromCamera) - - def gotShowWarningDownloadingFromCamera(self, dialog, showWarningAgain): - dialog.destroy() - self.prefs.show_warning_downloading_from_camera = showWarningAgain - - def getUseDevice(self, path, volume, autostart): - """ Prompt user whether or not to download from this device """ - - cmd_line(_("Prompting whether to use %s" % volume.get_name(limit=0))) - d = UseDeviceDialog(self.widget, path, volume, autostart, self.gotUseDevice) - - def gotUseDevice(self, dialog, userSelected, permanent_choice, path, volume, autostart): - """ User has chosen whether or not to use a device to download from """ - dialog.destroy() - - if userSelected: - if permanent_choice and path not in self.prefs.device_whitelist: - # do not do a list append operation here without the assignment, or the preferences will not be updated! - if len(self.prefs.device_whitelist): - self.prefs.device_whitelist = self.prefs.device_whitelist + [path] - else: - self.prefs.device_whitelist = [path] - self.initiateScan(path, volume, autostart) - - elif permanent_choice and path not in self.prefs.device_blacklist: - # do not do a list append operation here without the assignment, or the preferences will not be updated! - if len(self.prefs.device_blacklist): - self.prefs.device_blacklist = self.prefs.device_blacklist + [path] - else: - self.prefs.device_blacklist = [path] - - def _getJobCode(self, postJobCodeEntryCB, autoStart, downloadSelected): + + def _get_job_code(self, post_job_code_entry_callback, autoStart, downloadSelected): """ prompt for a job code """ if not self.prompting_for_job_code: - cmd_line(_("Prompting for Job Code")) + logger.debug("Prompting for Job Code") self.prompting_for_job_code = True - j = JobCodeDialog(self.widget, self.prefs.job_codes, self.last_chosen_job_code, postJobCodeEntryCB, autoStart, downloadSelected, False) + j = preferencesdialog.JobCodeDialog(parent_window = self.rapidapp, + job_codes = self.prefs.job_codes, + default_job_code = self.last_chosen_job_code, + post_job_code_entry_callback=post_job_code_entry_callback, + entry_only = False) else: - cmd_line(_("Already prompting for Job Code, do not prompt again")) + logger.debug("Already prompting for Job Code, do not prompt again") - def getJobCode(self, autoStart=True, downloadSelected=False): - """ called from the copyphotos thread, or when the user clicks one of the two download buttons""" - - self._getJobCode(self.gotJobCode, autoStart, downloadSelected) + def get_job_code(self): + self._get_job_code(self.got_job_code) - def gotJobCode(self, dialog, userChoseCode, code, autoStart, downloadSelected): + def got_job_code(self, dialog, user_chose_code, code): dialog.destroy() self.prompting_for_job_code = False - if userChoseCode: - self.assignJobCode(code) + if user_chose_code: + self.assign_job_code(code) self.last_chosen_job_code = code - self.selection_vbox.selection_treeview.apply_job_code(code, overwrite=False, to_all_rows = not downloadSelected) - threads = self.selection_vbox.selection_treeview.set_status_to_download_pending(selected_only = downloadSelected) - if downloadSelected or not autoStart: - cmd_line(_("Starting downloads")) - self.startDownload(threads) - else: - # autostart is true - cmd_line(_("Starting downloads that have been waiting for a Job Code")) - for w in workers.getWaitingForJobCodeWorkers(): - w.startStop() + #~ self.selection_vbox.selection_treeview.apply_job_code(code, overwrite=False, to_all_rows = not downloadSelected) + #~ threads = self.selection_vbox.selection_treeview.set_status_to_download_pending(selected_only = downloadSelected) + #~ if downloadSelected or not autoStart: + #~ logger.debug("Starting downloads") + #~ self.startDownload(threads) + #~ else: + #~ # autostart is true + #~ logger.debug("Starting downloads that have been waiting for a Job Code") + #~ for w in workers.getWaitingForJobCodeWorkers(): + #~ w.startStop() else: # user cancelled - for w in workers.getWaitingForJobCodeWorkers(): - w.waitingForJobCode = False - - if autoStart: - for w in workers.getAutoStartWorkers(): - w.autoStart = False - - def addFile(self, mediaFile): - self.selection_vbox.add_file(mediaFile) - - def update_status_post_download(self, treerowref): - self.selection_vbox.selection_treeview.update_status_post_download(treerowref) - - def on_menu_size_column_toggled(self, widget): - self.prefs.display_size_column = widget.get_active() - self.selection_vbox.selection_treeview.display_size_column(self.prefs.display_size_column) - - def on_menu_type_column_toggled(self, widget): - self.prefs.display_type_column = widget.get_active() - self.selection_vbox.selection_treeview.display_type_column(self.prefs.display_type_column) + pass + #~ logger.debug("No Job Code entered") + #~ for w in workers.getWaitingForJobCodeWorkers(): + #~ w.waitingForJobCode = False + #~ + #~ if autoStart: + #~ for w in workers.getAutoStartWorkers(): + #~ w.autoStart = False + + # # # + # Download + # # # + + def _init_download_tracking(self): + """ + Initialize variables to track downloads + """ + # Track download sizes and other values for each device. + # (Scan id acts as an index to each device. A device could be scanned + # more than once). + self.size_of_download_in_bytes_by_scan_pid = dict() + self.no_files_in_download_by_scan_pid = dict() + self.file_types_present_by_scan_pid = dict() + self.download_count_for_file_by_unique_id = dict() + self.download_count_by_scan_pid = dict() - def on_menu_filename_column_toggled(self, widget): - self.prefs.display_filename_column = widget.get_active() - self.selection_vbox.selection_treeview.display_filename_column(self.prefs.display_filename_column) + # Track which temporary directories are created when downloading files + self.temp_dirs_by_scan_pid = dict() - def on_menu_path_column_toggled(self, widget): - self.prefs.display_path_column = widget.get_active() - self.selection_vbox.selection_treeview.display_path_column(self.prefs.display_path_column) + # Track which downloads are running + self.download_active_by_scan_pid = [] - def on_menu_device_column_toggled(self, widget): - self.prefs.display_device_column = widget.get_active() - self.selection_vbox.selection_treeview.display_device_column(self.prefs.display_device_column) - - def checkIfFirstTimeProgramEverRun(self): + + + def start_download(self): """ - if this is the first time the program has been run, then - might need to create default directories + Start download, renaming and backup of files. """ - if len(self.prefs.program_version) == 0: - path = getDefaultPhotoLocation(ignore_missing_dir=True) - if not os.path.isdir(path): - cmd_line(_("Creating photo download folder %(folder)s") % {'folder':path}) - try: - os.makedirs(path) - self.prefs.download_folder = path - except: - cmd_line(_("Failed to create default photo download folder %(folder)s") % {'folder':path}) - if DOWNLOAD_VIDEO: - path = getDefaultVideoLocation(ignore_missing_dir=True) - if not os.path.isdir(path): - cmd_line(_("Creating video download folder %(folder)s") % {'folder':path}) - try: - os.makedirs(path) - self.prefs.video_download_folder = path - except: - cmd_line(_("Failed to create default video download folder %(folder)s") % {'folder':path}) - - def checkForUpgrade(self, runningVersion): - """ Checks if the running version of the program is different from the version recorded in the preferences. - - If the version is different, then the preferences are checked to see whether they should be upgraded or not. - returns True if program preferences window should be opened """ + self.download_start_time = datetime.datetime.now() + files_by_scan_pid = self.thumbnails.get_files_checked_for_download() + folders_valid = self.check_download_folder_validity(files_by_scan_pid) - displayPrefs = upgraded = False + #FIXME: if invalid, display some kind of error message to the user - previousVersion = self.prefs.program_version - if len(previousVersion) > 0: - # the program has been run previously for this user - - pv = common.pythonifyVersion(previousVersion) - rv = common.pythonifyVersion(runningVersion) - - title = PROGRAM_NAME - imageRename = subfolder = None - - if pv != rv: - if pv > rv: - prefsOk = rn.checkPreferencesForValidity(self.prefs.image_rename, self.prefs.subfolder, self.prefs.video_rename, self.prefs.video_subfolder) - - msg = _("A newer version of this program was previously run on this computer.\n\n") - if prefsOk: - msg += _("Program preferences appear to be valid, but please check them to ensure correct operation.") - else: - msg += _("Sorry, some preferences are invalid and will be reset.") - sys.stderr.write(_("Warning:") + " %s\n" % msg) - misc.run_dialog(title, msg) - displayPrefs = True + if folders_valid: + self.thumbnails.mark_download_pending(files_by_scan_pid) + for scan_pid in files_by_scan_pid: + files = files_by_scan_pid[scan_pid] + self.download_files(files, scan_pid) - else: - cmd_line(_("This version of the program is newer than the previously run version. Checking preferences.")) - - if rn.checkPreferencesForValidity(self.prefs.image_rename, self.prefs.subfolder, self.prefs.video_rename, self.prefs.video_subfolder, previousVersion): - upgraded, imageRename, subfolder = rn.upgradePreferencesToCurrent(self.prefs.image_rename, self.prefs.subfolder, previousVersion) - if upgraded: - self.prefs.image_rename = imageRename - self.prefs.subfolder = subfolder - cmd_line(_("Preferences were modified.")) - msg = _('This version of the program uses different preferences than the old version. Your preferences have been updated.\n\nPlease check them to ensure correct operation.') - misc.run_dialog(title, msg) - displayPrefs = True - else: - cmd_line(_("No preferences needed to be changed.")) - else: - msg = _('This version of the program uses different preferences than the old version. Some of your previous preferences were invalid, and could not be updated. They will be reset.') - sys.stderr.write(msg + "\n") - misc.run_dialog(title, msg) - displayPrefs = True - - - return displayPrefs - - def initPyNotify(self): - if not pynotify.init("TestCaps"): - sys.stderr.write(_("Problem using pynotify.") + "\n") - sys.exit(1) - - capabilities = {'actions': False, - 'body': False, - 'body-hyperlinks': False, - 'body-images': False, - 'body-markup': False, - 'icon-multi': False, - 'icon-static': False, - 'sound': False, - 'image/svg+xml': False, - 'append': False} - - caps = pynotify.get_server_caps () - if caps is None: - sys.stderr.write(_("Failed to receive pynotify server capabilities.") + "\n") - sys.exit (1) - - for cap in caps: - capabilities[cap] = True - - do_not_size_icon = False - self.notification_icon_size = 48 - try: - info = pynotify.get_server_info() - except: - cmd_line(_("Warning: desktop environment notification server is incorrectly configured.")) - else: - try: - if info["name"] == 'notify-osd': - do_not_size_icon = True - except: - pass - - if do_not_size_icon: - self.application_icon = gtk.gdk.pixbuf_new_from_file( - paths.share_dir('glade3/rapid-photo-downloader.svg')) - else: - self.application_icon = gtk.gdk.pixbuf_new_from_file_at_size( - paths.share_dir('glade3/rapid-photo-downloader.svg'), - self.notification_icon_size, self.notification_icon_size) - + self.set_download_action_label(is_download = False) - - def usingVolumeMonitor(self): - """ - Returns True if programs needs to use gio or gnomevfs volume monitor - """ + def pause_download(self): - return (self.prefs.device_autodetection or - (self.prefs.backup_images and - self.prefs.backup_device_autodetection - )) + self.copy_files_manager.pause() - - def startVolumeMonitor(self): - if not self.volumeMonitor: - self.volumeMonitor = VMonitor(self) - - def displayBackupVolumes(self): + # set action to display Download + if not self.download_action_is_download: + self.set_download_action_label(is_download = True) + + def resume_download(self): + self.copy_files_manager.start() + + def download_files(self, files, scan_pid): """ - Create a message to be displayed to the user showing which backup volumes will be used + Initiate downloading and renaming of files """ - message = '' - paths = self.backupVolumes.keys() - i = 0 - v = len(paths) - prefix = '' - for b in paths: - if v > 1: - if i < (v -1) and i > 0: - prefix = ', ' - elif i == (v - 1) : - prefix = " " + _("and") + " " - i += 1 - message = "%s%s'%s'" % (message, prefix, self.backupVolumes[b].get_name()) - - if v > 1: - message = _("Using backup devices") + " %s" % message - elif v == 1: - message = _("Using backup device") + " %s" % message + # Check which file types will be downloaded for this particular process + if self.files_of_type_present(files, rpdfile.FILE_TYPE_PHOTO): + photo_download_folder = self.prefs.download_folder else: - message = _("No backup devices detected") + photo_download_folder = None - return message - - def searchForPsd(self): + if self.files_of_type_present(files, rpdfile.FILE_TYPE_VIDEO): + video_download_folder = self.prefs.video_download_folder + else: + video_download_folder = None + + self.size_of_download_in_bytes_by_scan_pid[scan_pid] = self.size_files_to_be_downloaded(files) + self.no_files_in_download_by_scan_pid[scan_pid] = len(files) + self.download_active_by_scan_pid.append(scan_pid) + # Initiate copy files process + self.copy_files_manager.add_task((photo_download_folder, + video_download_folder, scan_pid, + files)) + + def copy_files_results(self, source, condition): """ - Check to see if user preferences are to automatically search for Portable Storage Devices or not + Handle results from copy files process """ - return self.prefs.device_autodetection_psd and self.prefs.device_autodetection - + #FIXME: must handle early termination / pause of copy files process + connection = self.copy_files_manager.get_pipe(source) + conn_type, msg_data = connection.recv() + if conn_type == rpdmp.CONN_PARTIAL: + msg_type, data = msg_data - def isGProxyShadowMount(self, gMount): - - """ gvfs GProxyShadowMount is used for the camera itself, not the data in the memory card """ - if using_gio: - if hasattr(gMount, 'is_shadowed'): - return gMount.is_shadowed() - else: - return str(type(gMount)).find('GProxyShadowMount') >= 0 + 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) + elif msg_type == rpdmp.MSG_BYTES: + scan_pid, total_downloaded = data + percent_complete = (float(total_downloaded) / + self.size_of_download_in_bytes_by_scan_pid[scan_pid]) * 100 + self.device_collection.update_progress(scan_pid, percent_complete, + None, None) + elif msg_type == rpdmp.MSG_FILE: + download_succeeded, rpd_file, download_count, temp_full_file_name = data + + + self.download_count_for_file_by_unique_id[rpd_file.unique_id] = download_count + self.download_count_by_scan_pid[rpd_file.scan_pid] = download_count + rpd_file.download_start_time = self.download_start_time + + if download_succeeded: + # Insert preference values needed for name generation + rpd_file = prefsrapid.insert_pref_lists(self.prefs, rpd_file) + rpd_file.strip_characters = self.prefs.strip_characters + rpd_file.download_folder = self.prefs.get_download_folder_for_file_type(rpd_file.file_type) + + #~ if not download_succeeded: + #~ logger.error("File was not downloaded: %s", rpd_file.full_file_name) + + self.subfolder_file_manager.rename_file_and_move_to_subfolder( + download_succeeded, + download_count, + rpd_file + ) + + + return True else: + # Process is complete, i.e. conn_type == rpdmp.CONN_COMPLETE + connection.close() return False - def isCamera(self, volume): - if using_gio: - try: - return volume.get_root().query_filesystem_info(gio.FILE_ATTRIBUTE_GVFS_BACKEND).get_attribute_as_string(gio.FILE_ATTRIBUTE_GVFS_BACKEND) == 'gphoto2' - except: - return False - else: - return False - def workerHasThisPath(self, path): - havePath= False - for w in workers.getNonFinishedWorkers(): - if w.cardMedia.path == path: - havePath = True - break - return havePath - - def on_volume_mounted(self, monitor, mount): - """ - callback run when gnomevfs indicates a new volume - has been mounted - """ - - if self.usingVolumeMonitor(): - volume = Volume(mount) - path = volume.get_path() - - if path in self.prefs.device_blacklist and self.searchForPsd(): - cmd_line(_("Device %(device)s (%(path)s) ignored") % { - 'device': volume.get_name(limit=0), 'path': path}) - else: - if not self.isGProxyShadowMount(mount): - self._printDetectedDevice(volume.get_name(limit=0), path) - - isBackupVolume = self.checkIfBackupVolume(path) - - if isBackupVolume: - if path not in self.backupVolumes: - self.backupVolumes[path] = volume - self.displayFreeSpace() - - elif self.prefs.device_autodetection and (media.is_DCIM_Media(path) or self.searchForPsd()): - if self.isCamera(volume.volume): - self.getShowWarningDownloadingFromCamera() - if self.searchForPsd() and path not in self.prefs.device_whitelist: - # prompt user if device should be used or not - self.getUseDevice(path, volume, self.prefs.auto_download_upon_device_insertion) - else: - self._printAutoStart(self.prefs.auto_download_upon_device_insertion) - self.initiateScan(path, volume, self.prefs.auto_download_upon_device_insertion) - - def initiateScan(self, path, volume, autostart): - """ initiates scan of image device""" - cardMedia = CardMedia(path, volume) - i = workers.getNextThread_id() - - workers.append(CopyPhotos(i, self, self.fileRenameLock, - self.fileSequenceLock, self.statsLock, - self.downloadedFilesLock, self.downloadStats, - autostart, cardMedia)) - - - self.setDownloadButtonSensitivity() - self.startScan() - - - def on_volume_unmounted(self, monitor, volume): - """ - callback run when gnomevfs indicates a volume - has been unmounted - """ - - v = Volume(volume) - path = v.get_path() - - # four scenarios - - # volume is waiting to be scanned - # the volume has been scanned but downloading has not yet started - # images are being downloaded from volume (it must be a messy unmount) - # images finished downloading from volume - - if path: - # first scenario - - for w in workers.getReadyToStartWorkers(): - if w.cardMedia.volume: - if w.cardMedia.volume.volume == volume: - media_collection_treeview.removeCard(w.thread_id) - self.selection_vbox.selection_treeview.clear_all(w.thread_id) - workers.disableWorker(w.thread_id) - # second scenario - for w in workers.getReadyToDownloadWorkers(): - if w.cardMedia.volume: - if w.cardMedia.volume.volume == volume: - media_collection_treeview.removeCard(w.thread_id) - self.selection_vbox.selection_treeview.clear_all(w.thread_id) - workers.disableWorker(w.thread_id) - - # fourth scenario - nothing to do - - # remove backup volumes - if path in self.backupVolumes: - del self.backupVolumes[path] - self.displayFreeSpace() - - # may need to disable download button - self.setDownloadButtonSensitivity() - - def clearCompletedDownloads(self): - """ - clears the display of completed downloads - """ - - for w in workers.getFinishedWorkers(): - media_collection_treeview.removeCard(w.thread_id) - self.selection_vbox.selection_treeview.clear_all(w.thread_id) - - - - - def clearNotStartedDownloads(self): - """ - Clears the display of the download and instructs the thread not to run + def download_is_occurring(self): + """Returns True if a file is currently being downloaded or renamed """ - - for w in workers.getNotDownloadingWorkers(): - media_collection_treeview.removeCard(w.thread_id) - workers.disableWorker(w.thread_id) + return not len(self.download_active_by_scan_pid) == 0 - def checkIfBackupVolume(self, path): - """ - Checks to see if backups are enabled and path represents a valid backup location - - Checks against user preferences. - """ - identifiers = [self.prefs.backup_identifier] - if DOWNLOAD_VIDEO: - identifiers.append(self.prefs.video_backup_identifier) - if self.prefs.backup_images: - if self.prefs.backup_device_autodetection: - if media.isBackupMedia(path, identifiers): - return True - elif path == self.prefs.backup_location: - # user manually specified the path - return True - return False - - def _printDetectedDevice(self, volume_name, path): - cmd_line (_("Detected %(device)s with path %(path)s") % {'device': volume_name, 'path': path}) - - def _printAutoStart(self, autoStart): - if autoStart: - cmd_line(_("Automatically start download is true") ) - else: - cmd_line(_("Automatically start download is false") ) - - def setupAvailableImageAndBackupMedia(self, onStartup, onPreferenceChange, doNotAllowAutoStart): - """ - Sets up volumes for downloading from and backing up to - - onStartup should be True if the program is still starting, i.e. this is being called from the - program's initialization. - - onPreferenceChange should be True if this is being called as the result of a preference - being changed - - Removes any image media that are currently not downloaded, - or finished downloading - """ - - self.clearNotStartedDownloads() - - volumeList = [] - self.backupVolumes = {} - - if not workers.noDownloadingWorkers(): - self.downloadStats.clear() - self._resetDownloadInfo() - - if self.usingVolumeMonitor(): - # either using automatically detected backup devices - # or download devices - - for v in self.volumeMonitor.get_mounts(): - volume = Volume(v) #'volumes' are actually mounts (legacy variable name at work here) - path = volume.get_path(avoid_gnomeVFS_bug = True) - - if path: - if path in self.prefs.device_blacklist and self.searchForPsd(): - cmd_line(_("Device %(device)s (%(path)s) ignored") % { - 'device': volume.get_name(limit=0), - 'path': path}) - else: - if not self.isGProxyShadowMount(v): - self._printDetectedDevice(volume.get_name(limit=0), path) - isBackupVolume = self.checkIfBackupVolume(path) - if isBackupVolume: - #backupPath = os.path.join(path, self.prefs.backup_identifier) - self.backupVolumes[path] = volume - elif self.prefs.device_autodetection and (media.is_DCIM_Media(path) or self.searchForPsd()): - volumeList.append((path, volume)) - - - if not self.prefs.device_autodetection: - # user manually specified the path from which to download - path = self.prefs.device_location - if path: - cmd_line(_("Using manually specified path") + " %s" % path) - volumeList.append((path, None)) - - if self.prefs.backup_images: - if not self.prefs.backup_device_autodetection: - # user manually specified backup location - # will backup to this path, but don't need any volume info associated with it - self.backupVolumes[self.prefs.backup_location] = None + # # # + # Create folder and file names for downloaded files + # # # + + def subfolder_file_results(self, move_succeeded, rpd_file): + """ + Handle results of subfolder creation and file renaming + """ + + scan_pid = rpd_file.scan_pid + unique_id = rpd_file.unique_id - self.displayFreeSpace() - # add each memory card / other device to the list of threads + self._update_file_download_device_progress(scan_pid, unique_id) - if doNotAllowAutoStart: - autoStart = False - else: - autoStart = (not onPreferenceChange) and ((self.prefs.auto_download_at_startup and onStartup) or (self.prefs.auto_download_upon_device_insertion and not onStartup)) + self.thumbnails.update_status_post_download(rpd_file) - self._printAutoStart(autoStart) + download_count = self.download_count_for_file_by_unique_id[unique_id] + if download_count == self.no_files_in_download_by_scan_pid[scan_pid]: + # Last file has been downloaded, so clean temp directory + logger.debug("Purging temp directories") + self._clean_temp_dirs_for_scan_pid(scan_pid) + del self.no_files_in_download_by_scan_pid[scan_pid] + del self.size_of_download_in_bytes_by_scan_pid[scan_pid] + self.download_active_by_scan_pid.remove(scan_pid) + + if not self.download_is_occurring(): + logger.debug("Download completed") + + 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) + + self.display_free_space() + + self.set_download_action_label(is_download=True) + self.set_download_action_sensitivity() + else: + pass + #~ logger.info("Download count: %s", download_count) + + - shownWarning = False + def _update_file_download_device_progress(self, scan_pid, unique_id): + """ + Increments the progress bar for an individual device + """ + #~ scan_pid = rpd_file.scan_pid + #~ unique_id = rpd_file.unique_id + progress_bar_text = _("%(number)s of %(total)s %(filetypes)s") % \ + {'number': self.download_count_for_file_by_unique_id[unique_id], + 'total': self.no_files_in_download_by_scan_pid[scan_pid], + 'filetypes': self.file_types_present_by_scan_pid[scan_pid]} + self.device_collection.update_progress(scan_pid, None, progress_bar_text, None) - for i in range(len(volumeList)): - path, volume = volumeList[i] - if volume: - if self.isCamera(volume.volume) and not shownWarning: - self.getShowWarningDownloadingFromCamera() - shownWarning = True - if self.searchForPsd() and path not in self.prefs.device_whitelist: - # prompt user to see if device should be used or not - self.getUseDevice(path, volume, autoStart) - else: - self.initiateScan(path, volume, autoStart) + def _clean_all_temp_dirs(self): + """ + Cleans all temp dirs if they exist + """ + for scan_pid in self.temp_dirs_by_scan_pid: + for temp_dir in self.temp_dirs_by_scan_pid[scan_pid]: + self._purge_dir(temp_dir) - def refreshBackupMedia(self): + self.temp_dirs_by_scan_pid = {} + + + def _clean_temp_dirs_for_scan_pid(self, scan_pid): """ - Setup the backup media + Deletes temp files and folders used in download + """ + for temp_dir in self.temp_dirs_by_scan_pid[scan_pid]: + self._purge_dir(temp_dir) + del self.temp_dirs_by_scan_pid[scan_pid] + + def _purge_dir(self, directory): + """ + Deletes all files in the directory, and the directory itself. - Assumptions: this is being called after the user has changed their preferences AND download media has already been setup + Does not recursively traverse any subfolders in the directory. """ - self.backupVolumes = {} - if self.prefs.backup_images: - if not self.prefs.backup_device_autodetection: - # user manually specified backup location - # will backup to this path, but don't need any volume info associated with it - self.backupVolumes[self.prefs.backup_location] = None - else: - for v in self.volumeMonitor.get_mounts(): - volume = Volume(v) - path = volume.get_path(avoid_gnomeVFS_bug = True) - if path: - if self.checkIfBackupVolume(path): - # is a backup volume - if path not in self.backupVolumes: - # ensure it is not in a list of workers which have not started downloading - # if it is, remove it - for w in workers.getNotDownloadingAndNotFinishedWorkers(): - if w.cardMedia.path == path: - media_collection_treeview.removeCard(w.thread_id) - self.selection_vbox.selection_treeview.clear_all(w.thread_id) - workers.disableWorker(w.thread_id) - - downloading_workers = [] - for w in workers.getDownloadingWorkers(): - downloading_workers.append(w) - - for w in downloading_workers: - if w.cardMedia.path == path: - # the user is trying to backup to a device that is currently being downloaded from..... we don't normally allow that, but what to do? - cmd_line(_("Warning: backup device %(device)s is currently being downloaded from") % {'device': volume.get_name(limit=0)}) - - self.backupVolumes[path] = volume - - self.displayFreeSpace() - def _setupDownloadbuttons(self): - self.download_hbutton_box = gtk.HButtonBox() - self.download_hbutton_box.set_spacing(12) - self.download_hbutton_box.set_homogeneous(False) - - help_button = gtk.Button(stock=gtk.STOCK_HELP) - help_button.connect("clicked", self.on_help_button_clicked) - self.download_hbutton_box.pack_start(help_button) - self.download_hbutton_box.set_child_secondary(help_button, True) + if directory: + try: + path = gio.File(directory) + # first delete any files in the temp directory + # assume there are no directories in the temp directory + file_attributes = "standard::name,standard::type" + children = path.enumerate_children(file_attributes) + for child in children: + f = path.get_child(child.get_name()) + #~ logger.info("Deleting %s", child.get_name()) + f.delete(cancellable=None) + path.delete(cancellable=None) + logger.debug("Deleted directory %s", directory) + except gio.Error, inst: + logger.error("Failure deleting temporary folder %s", directory) + logger.error(inst) - self.DOWNLOAD_SELECTED_LABEL = _("D_ownload Selected") - self.download_button_is_download = True - self.download_button = gtk.Button() - self.download_button.set_use_underline(True) - self.download_button.set_flags(gtk.CAN_DEFAULT) - self.download_selected_button = gtk.Button() - self.download_selected_button.set_use_underline(True) - self._set_download_button() - self.download_button.connect('clicked', self.on_download_button_clicked) - self.download_selected_button.connect('clicked', self.on_download_selected_button_clicked) - self.download_hbutton_box.set_layout(gtk.BUTTONBOX_END) - self.download_hbutton_box.pack_start(self.download_selected_button) - self.download_hbutton_box.pack_start(self.download_button) - self.download_hbutton_box.show_all() - self.buttons_hbox.pack_start(self.download_hbutton_box, - padding=hd.WINDOW_BORDER_SPACE) - - self.setDownloadButtonSensitivity() - - def set_display_selection(self, value): - if value: - self.selection_vbox.preview_table.show_all() - else: - self.selection_vbox.preview_table.hide() - self.selection_vbox.set_display_preview_folders(self.prefs.display_preview_folders) - - def set_display_preview_folders(self, value): - self.selection_vbox.set_display_preview_folders(value) - - def _resetDownloadInfo(self): - self.markSet = False - self.startTime = None - self.totalDownloadSize = self.totalDownloadedSoFar = 0 - self.totalDownloadSizeThisRun = self.totalDownloadedSoFarThisRun = 0 - # there is no need to clear self.timeRemaining, as when each thread is completed, it removes itself - - # this next value is used by the date time option "Download Time" - self.download_start_time = None - - global job_code - job_code = None - def addToTotalDownloadSize(self, size): - self.totalDownloadSize += size - - def setOverallDownloadMark(self): - if not self.markSet: - self.markSet = True - self.totalDownloadSizeThisRun = self.totalDownloadSize - self.totalDownloadedSoFar - self.totalDownloadedSoFarThisRun = 0 - - self.startTime = time.time() - self.timeStatusBarUpdated = self.startTime + # # # + # Preferences + # # # - self.timeMark = self.startTime - self.sizeMark = 0 + def check_prefs_on_startup(self): + """ + Checks the image & video rename, and subfolder prefs for validity. + + Returns True if no problem, false otherwise. + """ + prefs_ok = prefsrapid.check_prefs_for_validity(self.prefs.image_rename, + self.prefs.subfolder, + self.prefs.video_rename, + self.prefs.video_subfolder) + if not prefs_ok: + logger.error("There is an error in the program preferences. Some preferences will be reset.") + return prefs_ok - def startOrResumeWorkers(self, threads): - - # resume any paused workers - for w in workers.getPausedDownloadingWorkers(): - w.startStop() - self.timeRemaining.setTimeMark(w) - # set the time that the download started - this is used - # in the "Download Time" date time renaming option. - self.setDownloadStartTime() + def _init_prefs(self): + self.prefs = prefsrapid.RapidPreferences() + self.prefs.notify_add(self.on_preference_changed) + + # flag to indicate whether the user changed some preferences that + # indicate the image and backup devices should be setup again + self.rerun_setup_available_image_and_video_media = False + self.rerun_setup_available_backup_media = False + + # flag to indicate that the preferences dialog window is being + # displayed to the user + self.preferences_dialog_displayed = False - - #start any new workers that have downloads pending - for i in threads: - workers[i].startStop() + # flag to indicate that the user has modified the download today + # related values in the preferences dialog window + self.refresh_downloads_today = False - if is_beta and verbose and False: - workers.printWorkerStatus() - - def setDownloadStartTime(self): - if not self.download_start_time: - self.download_start_time = datetime.datetime.now() + self.downloads_today_tracker = self.prefs.get_downloads_today_tracker() + + downloads_today = self.downloads_today_tracker.get_and_maybe_reset_downloads_today() + if downloads_today > 0: + logger.info("Downloads that have occurred so far today: %s", downloads_today) + else: + logger.info("No downloads have occurred so far today") + + self.downloads_today_value = Value(c_int, + self.downloads_today_tracker.get_raw_downloads_today()) + self.downloads_today_date_value = Array(c_char, + self.downloads_today_tracker.get_raw_downloads_today_date()) + self.day_start_value = Array(c_char, + self.downloads_today_tracker.get_raw_day_start()) + self.refresh_downloads_today_value = Value(c_bool, False) + self.stored_sequence_value = Value(c_int, self.prefs.stored_sequence_no) + self.uses_stored_sequence_no_value = Value(c_bool, self.prefs.any_pref_uses_stored_sequence_no()) + self.uses_session_sequece_no_value = Value(c_bool, self.prefs.any_pref_uses_session_sequece_no()) + self.uses_sequence_letter_value = Value(c_bool, self.prefs.any_pref_uses_sequence_letter_value()) - def updateOverallProgress(self, thread_id, bytesDownloaded, percentComplete): + self.prefs.program_version = __version__ + + def _check_for_sequence_value_use(self): + self.uses_stored_sequence_no_value.value = self.prefs.any_pref_uses_stored_sequence_no() + self.uses_session_sequece_no_value.value = self.prefs.any_pref_uses_session_sequece_no() + self.uses_sequence_letter_value.value = self.prefs.any_pref_uses_sequence_letter_value() + + def on_preference_changed(self, key, value): """ - Updates progress bar and status bar text with time remaining - to download images + Called when user changes the program's preferences """ + logger.debug("Preference change detected: %s", key) + + + if key == 'show_log_dialog': + self.menu_log_window.set_active(value) + elif key in ['device_autodetection', 'device_autodetection_psd', 'device_location']: + self.rerun_setup_available_image_and_video_media = True + if not self.preferences_dialog_displayed: + self.post_preference_change() - self.totalDownloadedSoFar += bytesDownloaded - self.totalDownloadedSoFarThisRun += bytesDownloaded - - fraction = self.totalDownloadedSoFar / float(self.totalDownloadSize) - - self.download_progressbar.set_fraction(fraction) - - if percentComplete == 100.0: - self.menu_clear.set_sensitive(True) - self.timeRemaining.remove(thread_id) - - if self.downloadComplete(): - # finished all downloads - self.rapid_statusbar.push(self.statusbar_context_id, "") - self.download_button_is_download = True - self._set_download_button() - self.setDownloadButtonSensitivity() - cmd_line(_("All downloads complete")) - job_code = None - if is_beta and verbose and False: - workers.printWorkerStatus() - - else: - now = time.time() - self.timeRemaining.update(thread_id, bytesDownloaded) - - if now > (self.downloadTimeGap + self.timeMark): - amtTime = now - self.timeMark - self.timeMark = now - amtDownloaded = self.totalDownloadedSoFarThisRun - self.sizeMark - self.sizeMark = self.totalDownloadedSoFarThisRun - amtToDownload = float(self.totalDownloadSizeThisRun) - self.totalDownloadedSoFarThisRun - downloadSpeed = "%1.1f" % (amtDownloaded / 1048576 / amtTime) +_("MB/s") - self.speed_label.set_text(downloadSpeed) + elif key in ['backup_images', 'backup_device_autodetection', 'backup_location', 'backup_identifier', 'video_backup_identifier']: + self.rerun_setup_available_backup_media = True + if not self.preferences_dialog_displayed: + self.post_preference_change() - timeRemaining = self.timeRemaining.timeRemaining() - if timeRemaining: - secs = int(timeRemaining) + # 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 - if secs == 0: - message = "" - elif secs == 1: - message = _("About 1 second remaining") - elif secs < 60: - message = _("About %i seconds remaining") % secs - elif secs == 60: - message = _("About 1 minute remaining") - else: - # Translators: in the text '%(minutes)i:%(seconds)02i', only the : should be translated, if needed. - # '%(minutes)i' and '%(seconds)02i' should not be modified or left out. They are used to format and display the amount - # of time the download has remainging, e.g. 'About 5:36 minutes remaining' - message = _("About %(minutes)i:%(seconds)02i minutes remaining") % {'minutes': secs / 60, 'seconds': secs % 60} - - self.rapid_statusbar.pop(self.statusbar_context_id) - self.rapid_statusbar.push(self.statusbar_context_id, message) - - - def resetSequences(self): - if self.downloadComplete(): - sequences.reset(self.prefs.getDownloadsToday(), self.prefs.stored_sequence_no) + 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']: + # Check if stored sequence no is being used + self._check_for_sequence_value_use() + + #~ elif key == 'job_codes': + #~ # update job code list in left pane + #~ self.selection_vbox.update_job_code_combo() + + elif key in ['download_folder', 'video_download_folder']: + self.display_free_space() - def notifyUserAllDownloadsComplete(self): - """ If all downloads are complete, if needed notify the user using libnotify - - Reset progress bar info""" - - if self.downloadComplete(): - if self.displayDownloadSummaryNotification: - message = _("All downloads complete") - if self.downloadStats.noImagesDownloaded: - filetype = file_types_by_number(self.downloadStats.noImagesDownloaded, 0) - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noImagesDownloaded, - 'numberdownloaded': _("%(filetype)s downloaded") % \ - {'filetype': filetype}} - if self.downloadStats.noImagesSkipped: - filetype = file_types_by_number(self.downloadStats.noImagesSkipped, 0) - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noImagesSkipped, - 'numberdownloaded': _("%(filetype)s failed to download") % \ - {'filetype': filetype}} - if self.downloadStats.noVideosDownloaded: - filetype = file_types_by_number(0, self.downloadStats.noVideosDownloaded) - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noVideosDownloaded, - 'numberdownloaded': _("%(filetype)s downloaded") % \ - {'filetype': filetype}} - if self.downloadStats.noVideosSkipped: - filetype = file_types_by_number(0, self.downloadStats.noVideosSkipped) - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noVideosSkipped, - 'numberdownloaded': _("%(filetype)s failed to download") % \ - {'filetype': filetype}} - if self.downloadStats.noWarnings: - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noWarnings, - 'numberdownloaded': _("warnings")} - if self.downloadStats.noErrors: - message += "\n" + _("%(number)s %(numberdownloaded)s") % \ - {'number': self.downloadStats.noErrors, - 'numberdownloaded': _("errors")} - - n = pynotify.Notification(PROGRAM_NAME, message) - n.set_icon_from_pixbuf(self.application_icon) - n.show() - self.displayDownloadSummaryNotification = False # don't show it again unless needed - # download statistics are cleared in exitOnDownloadComplete() - self._resetDownloadInfo() - self.speed_label.set_text(' ') - self.displayFreeSpace() + def post_preference_change(self): + if self.rerun_setup_available_image_and_video_media: + if self.using_volume_monitor(): + self.start_volume_monitor() + logger.info("Download device settings preferences were changed.") + self.thumbnails.clear_all() + self.setup_devices(on_startup = False, on_preference_change = True, do_not_allow_auto_start = True) - def exitOnDownloadComplete(self): - if self.downloadComplete(): - if self.prefs.auto_exit: - if not (self.downloadStats.noErrors or self.downloadStats.noWarnings): - self.quit() - # since for whatever reason am not exiting, clear the download statistics - self.downloadStats.clear() - + self.rerun_setup_available_image_and_video_media = False + + if self.rerun_setup_available_backup_media: + if self.using_volume_monitor(): + self.start_volume_monitor() + logger.info("Backup preferences were changed.") + + logger.info("self.refreshBackupMedia()") + + 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) + + - def downloadFailed(self, thread_id): - if workers.noDownloadingWorkers() == 0: - self.download_button_is_download = True - self._set_download_button() - self.setDownloadButtonSensitivity() + # # # + # Main app window management and setup + # # # - def downloadComplete(self): - return self.totalDownloadedSoFar == self.totalDownloadSize - - def setDownloadButtonSensitivity(self): - - isSensitive = (workers.noReadyToDownloadWorkers() > 0 and - workers.noScanningWorkers() == 0 and - self.selection_vbox.selection_treeview.rows_available_for_download()) or \ - workers.noDownloadingWorkers() > 0 + def _init_widgets(self): + """ + Initialize widgets in the main window, and variables that point to them + """ + builder = gtk.Builder() + self.builder = builder + builder.add_from_file(paths.share_dir("glade3/rapid.ui")) + self.rapidapp = builder.get_object("rapidapp") + self.main_vpaned = builder.get_object("main_vpaned") + self.main_notebook = builder.get_object("main_notebook") + self.download_action = builder.get_object("download_action") - if isSensitive: - self.download_button.props.sensitive = True - # download selected button sensitity is enabled only when the user selects something - self.selection_vbox.selection_treeview.update_download_selected_button() - self.menu_download_pause.props.sensitive = True - else: - self.download_button.props.sensitive = False - self.download_selected_button.props.sensitive = False - self.menu_download_pause.props.sensitive = False - - return isSensitive + self.download_progressbar = builder.get_object("download_progressbar") + self.rapid_statusbar = builder.get_object("rapid_statusbar") + self.statusbar_context_id = self.rapid_statusbar.get_context_id("progress") + self.device_collection_scrolledwindow = builder.get_object("device_collection_scrolledwindow") + self.next_image_action = builder.get_object("next_image_action") + self.prev_image_action = builder.get_object("prev_image_action") + # Only enable this action when actually displaying a preview + self.next_image_action.set_sensitive(False) + self.prev_image_action.set_sensitive(False) - def on_rapidapp_destroy(self, widget): - """Called when the application is going to quit""" + # About dialog + builder.add_from_file(paths.share_dir("glade3/about.ui")) + self.about = builder.get_object("about") - # save window and component sizes - self.prefs.hpaned_pos = self.selection_vbox.file_hpaned.get_position() - self.prefs.vpaned_pos = self.main_vpaned.get_position() - - x, y = self.rapidapp.get_size() - self.prefs.main_window_size_x = x - self.prefs.main_window_size_y = y - - workers.quitAllWorkers() - - self.flushevents() + builder.connect_signals(self) - display_queue.close("w") - + self.preview_image = PreviewImage(self, builder) - def on_rapidapp_window_state_event(self, widget, event): - """ Checkto see if the user maximized the main application window or not. """ - if event.changed_mask & gdk.WINDOW_STATE_MAXIMIZED: - self.prefs.main_window_maximized = event.new_window_state & gdk.WINDOW_STATE_MAXIMIZED - - - def on_menu_clear_activate(self, widget): - self.clearCompletedDownloads() - widget.set_sensitive(False) + thumbnails_scrolledwindow = builder.get_object('thumbnails_scrolledwindow') + self.thumbnails = ThumbnailDisplay(self) + thumbnails_scrolledwindow.add(self.thumbnails) - def on_menu_refresh_activate(self, widget): - self.selection_vbox.selection_treeview.clear_all() - self.setupAvailableImageAndBackupMedia(onStartup = False, onPreferenceChange = True, doNotAllowAutoStart = True) + #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) - def on_menu_report_problem_activate(self, widget): - webbrowser.open("https://bugs.launchpad.net/rapid") + # 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 = {} - def on_menu_get_help_online_activate(self, widget): - webbrowser.open("http://www.damonlynch.net/rapid/help.html") - - def on_menu_donate_activate(self, widget): - webbrowser.open("http://www.damonlynch.net/rapid/donate.html") - - def on_menu_translate_activate(self, widget): - webbrowser.open("http://www.damonlynch.net/rapid/translate.html") - - def on_menu_preferences_activate(self, widget): - """ Sets preferences for the application using dialog window """ + # Download action state + self.download_action_is_download = True + + #job code initialization + self.last_chosen_job_code = None + self.prompting_for_job_code = False - PreferencesDialog(self) + def _set_window_size(self): + """ + Remember the window size from the last time the program was run, or + set a default size + """ - def on_menu_log_window_toggled(self, widget): - active = widget.get_active() - self.prefs.show_log_dialog = active - if active: - log_dialog.widget.show() + if self.prefs.main_window_maximized: + self.rapidapp.maximize() + self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH, + config.DEFAULT_WINDOW_HEIGHT) + elif self.prefs.main_window_size_x > 0: + self.rapidapp.set_default_size(self.prefs.main_window_size_x, self.prefs.main_window_size_y) else: - log_dialog.widget.hide() + # set a default size + self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH, + config.DEFAULT_WINDOW_HEIGHT) + - def on_menu_display_selection_toggled(self, check_button): - self.prefs.display_selection = check_button.get_active() + def _set_device_collection_size(self): + """ + Set the size of the device collection scrolled window widget + """ - def on_menu_preview_folders_toggled(self, check_button): - self.prefs.display_preview_folders = check_button.get_active() - def on_menu_zoom_out_activate(self, widget): - self.selection_vbox.zoom_out() + if self.device_collection.map_process_to_row: + height = self.device_collection_viewport.size_request()[1] + self.device_collection_scrolledwindow.set_size_request(-1, height) + else: + # don't allow the media collection to be absolutely empty + self.device_collection_scrolledwindow.set_size_request(-1, 47) - def on_menu_zoom_in_activate(self, widget): - self.selection_vbox.zoom_in() + + def on_rapidapp_window_state_event(self, widget, event): + """ Records the window maximization state in the preferences.""" - def on_menu_select_all_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('all') - - def on_menu_select_all_photos_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('photos') - - def on_menu_select_all_videos_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('videos') + if event.changed_mask & gdk.WINDOW_STATE_MAXIMIZED: + self.prefs.main_window_maximized = event.new_window_state & gdk.WINDOW_STATE_MAXIMIZED - def on_menu_select_none_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('none') + 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) - def on_menu_select_all_with_job_code_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('withjobcode') - - def on_menu_select_all_without_job_code_activate(self, widget): - self.selection_vbox.selection_treeview.select_rows('withoutjobcode') - - - def on_menu_about_activate(self, widget): - """ Display about dialog box """ - - about = gtk.glade.XML(paths.share_dir(config.GLADE_FILE), "about").get_widget("about") - about.set_property("name", PROGRAM_NAME) - about.set_property("version", __version__) - about.run() - about.destroy() - - def _set_download_button(self): + preview_button = self.builder.get_object("preview_button") + image = gtk.image_new_from_file(paths.share_dir('glade3/photo_icon.png')) + preview_button.set_image(image) + + next_image_button = self.builder.get_object("next_image_button") + image = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_BUTTON) + next_image_button.set_image(image) + + prev_image_button = self.builder.get_object("prev_image_button") + image = gtk.image_new_from_stock(gtk.STOCK_GO_BACK, gtk.ICON_SIZE_BUTTON) + prev_image_button.set_image(image) + + def _setup_icons(self): + icons = ['rapid-photo-downloader-jobcode',] + icon_list = [(icon, paths.share_dir('glade3/%s.svg' % icon)) for icon in icons] + register_iconsets(icon_list) + + def _setup_error_icons(self): """ - Sets download button to appropriate state + hide display of warning and error symbols in the taskbar until they + are needed """ + self.error_image = self.builder.get_object("error_image") + self.warning_image = self.builder.get_object("warning_image") + self.warning_vseparator = self.builder.get_object("warning_vseparator") + self.error_image.hide() + self.warning_image.hide() + self.warning_vseparator.hide() - if self.download_button_is_download: - # This text will be displayed to the user on the Download / Pause button. - self.download_selected_button.set_label(self.DOWNLOAD_SELECTED_LABEL) - self.download_selected_button.set_image(gtk.image_new_from_stock( - gtk.STOCK_CONVERT, - gtk.ICON_SIZE_BUTTON)) - self.selection_vbox.selection_treeview.update_download_selected_button() - - self.download_button.set_image(gtk.image_new_from_stock( - gtk.STOCK_CONVERT, - gtk.ICON_SIZE_BUTTON)) - - if workers.noPausedWorkers(): - self.download_button.set_label(_("_Resume")) - self.download_selected_button.hide() + 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_free_space(self): + """ + Displays the amount of space free on the filesystem the files will be + downloaded to. + + Also displays backup volumes / path being used. (NOT IMPLEMENTED YET) + """ + photo_dir = self.is_valid_download_dir(self.prefs.download_folder) + video_dir = self.is_valid_download_dir(self.prefs.video_download_folder) + if photo_dir and video_dir: + same_file_system = self.same_file_system(self.prefs.download_folder, + self.prefs.video_download_folder) + else: + same_file_system = False + + dirs = [] + if photo_dir: + dirs.append((self.prefs.download_folder, _("photos"))) + if video_dir and not same_file_system: + dirs.append((self.prefs.video_download_folder, _("videos"))) + + if len(dirs) > 1: + msg = ' ' + _('Free space:') + ' ' + + for i in range(len(dirs)): + dir_info = dirs[i] + folder = gio.File(dir_info[0]) + file_info = folder.query_filesystem_info(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE) + size = file_info.get_attribute_uint64(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE) + free = format_size_for_user(bytes=size) + if len(dirs) > 1: + #(videos) or (photos) will be appended to the free space message displayed to the + #user in the status bar. + #you should only translate this if your language does not use parantheses + file_type = _("(%(file_type)s)") % {'file_type': dir_info[1]} + + #Freespace available on the filesystem for downloading to + #Displayed in status bar message on main window + msg += _("%(free)s %(file_type)s") % {'free': free, 'file_type': file_type} + if i == 0: + #Inserted in the middle of the statusbar message concerning the amount of freespace + #Used to differentiate between two different file systems + #e.g. Free space: 21.3GB (photos); 14.7GB (videos). + msg += _("; ") + else: + #Inserted at the end of the statusbar message concerning the amount of freespace + #Used to differentiate between two different file systems + #e.g. Free space: 21.3GB (photos); 14.7GB (videos). + msg += _(".") + else: - self.download_button.set_label(_("_Download All")) - self.download_selected_button.show_all() - - else: - # button should indicate paused state - self.download_button.set_image(gtk.image_new_from_stock( - gtk.STOCK_MEDIA_PAUSE, - gtk.ICON_SIZE_BUTTON)) - # This text will be displayed to the user on the Download / Pause button. - self.download_button.set_label(_("_Pause")) - self.download_selected_button.set_sensitive(False) - self.download_selected_button.hide() - - def on_menu_download_pause_activate(self, widget): - self.on_download_button_clicked(widget) + #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} - def startScan(self): - if workers.noReadyToStartWorkers() > 0: - workers.startWorkers() - - def postStartDownloadTasks(self): - if workers.noDownloadingWorkers() > 1: - self.displayDownloadSummaryNotification = True - # set button to display Pause - self.download_button_is_download = False - self._set_download_button() - - def startDownload(self, threads): - self.startOrResumeWorkers(threads) - self.postStartDownloadTasks() - - def pauseDownload(self): - for w in workers.getDownloadingWorkers(): - w.startStop() - # set button to display Download - if not self.download_button_is_download: - self.download_button_is_download = True - self._set_download_button() - - def on_download_button_clicked(self, widget): - """ - Handle download button click. - - Button is in one of three states: download all, resume, or pause. - - If download, a click indicates to start or resume a download run. - If pause, a click indicates to pause all running downloads. - """ - if self.download_button_is_download: - if need_job_code_for_renaming and self.selection_vbox.selection_treeview.job_code_missing(False) and not self.prompting_for_job_code: - self.getJobCode(autoStart=False, downloadSelected=False) + if self.prefs.backup_images and False: #FIXME: skip this for now! + if not self.prefs.backup_device_autodetection: + # user manually specified backup location + msg2 = _('Backing up to %(path)s') % {'path':self.prefs.backup_location} else: - threads = self.selection_vbox.selection_treeview.set_status_to_download_pending(selected_only = False) - self.startDownload(threads) - self._set_download_button() - else: - self.pauseDownload() - - def on_download_selected_button_clicked(self, widget): - # set the status of the selected workers to be downloading pending - if need_job_code_for_renaming and self.selection_vbox.selection_treeview.job_code_missing(True) and not self.prompting_for_job_code: - self.getJobCode(autoStart=False, downloadSelected=True) - else: - threads = self.selection_vbox.selection_treeview.set_status_to_download_pending(selected_only = True) - self.startDownload(threads) + msg2 = self.displayBackupVolumes() - + if msg: + msg = _("%(freespace)s. %(backuppaths)s.") % {'freespace': msg, 'backuppaths': msg2} + else: + msg = msg2 - def on_help_button_clicked(self, widget): - webbrowser.open("http://www.damonlynch.net/rapid/help.html") + msg = msg.rstrip() - def on_preference_changed(self, key, value): + self.statusbar_message(msg) + + # # # + # Utility functions + # # # + + def files_of_type_present(self, files, file_type): """ - Called when user changes the program's preferences + Returns true if there is at least one instance of the file_type + in the list of files to be copied """ + for rpd_file in files: + if rpd_file.file_type == file_type: + return True + return False - if key == 'display_selection': - self.set_display_selection(value) - elif key == 'display_preview_folders': - self.set_display_preview_folders(value) - elif key == 'show_log_dialog': - self.menu_log_window.set_active(value) - elif key in ['device_autodetection', 'device_autodetection_psd', 'device_location']: - self.rerunSetupAvailableImageAndVideoMedia = True - if not self.preferencesDialogDisplayed: - self.postPreferenceChange() - - elif key in ['backup_images', 'backup_device_autodetection', 'backup_location', 'backup_identifier', 'video_backup_identifier']: - self.rerunSetupAvailableBackupMedia = True - if not self.preferencesDialogDisplayed: - self.postPreferenceChange() - - elif key in ['subfolder', 'image_rename', 'video_subfolder', 'video_rename']: - global need_job_code_for_renaming - need_job_code_for_renaming = self.needJobCodeForRenaming() - self.selection_vbox.set_job_code_display() - self.menu_select_all_without_job_code.set_sensitive(need_job_code_for_renaming) - self.menu_select_all_with_job_code.set_sensitive(need_job_code_for_renaming) - self.refreshGeneratedSampleSubfolderAndName = True - - if not self.preferencesDialogDisplayed: - self.postPreferenceChange() - - elif key in ['download_folder', 'video_download_folder']: - self.refreshSampleDownloadFolder = True - if not self.preferencesDialogDisplayed: - self.postPreferenceChange() - - elif key == 'job_codes': - # update job code list in left pane - self.selection_vbox.update_job_code_combo() - - - def postPreferenceChange(self): + def size_files_to_be_downloaded(self, files): """ - Handle changes in program preferences after the preferences dialog window has been closed + Returns the total size of the files to be downloaded in bytes """ - if self.rerunSetupAvailableImageAndVideoMedia: - if self.usingVolumeMonitor(): - self.startVolumeMonitor() - cmd_line("\n" + _("Download device settings preferences were changed.")) + size = 0 + for i in range(len(files)): + size += files[i].size + + return 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. + """ + valid = True + # first, check what needs to be downloaded - photos and / or videos + need_photo_folder = False + need_video_folder = False + while not need_photo_folder and not need_video_folder: + for scan_pid in files_by_scan_pid: + files = files_by_scan_pid[scan_pid] + if not need_photo_folder: + if self.files_of_type_present(files, rpdfile.FILE_TYPE_PHOTO): + need_photo_folder = True + if not need_video_folder: + if self.files_of_type_present(files, rpdfile.FILE_TYPE_VIDEO): + need_video_folder = True - self.selection_vbox.selection_treeview.clear_all() - self.setupAvailableImageAndBackupMedia(onStartup = False, onPreferenceChange = True, doNotAllowAutoStart = True) - if is_beta and verbose and False: - workers.printWorkerStatus() + # second, check validity + if need_photo_folder: + if not self.is_valid_download_dir(self.prefs.download_folder): + valid = False - self.rerunSetupAvailableImageAndVideoMedia = False - - if self.rerunSetupAvailableBackupMedia: - if self.usingVolumeMonitor(): - self.startVolumeMonitor() - cmd_line("\n" + _("Backup preferences were changed.")) - - self.refreshBackupMedia() - self.rerunSetupAvailableBackupMedia = False - - if self.refreshGeneratedSampleSubfolderAndName: - cmd_line("\n" + _("Subfolder and filename preferences were changed.")) - for w in workers.getScanningWorkers(): - if not w.scanResultsStale: - w.scanResultsStale = True - self.noAfterScanRefreshGeneratedSampleSubfolderAndName += 1 + if need_video_folder: + if not self.is_valid_download_dir(self.prefs.video_download_folder): + valid = False - self.selection_vbox.selection_treeview.refreshGeneratedSampleSubfolderAndName() - self.refreshGeneratedSampleSubfolderAndName = False - self.setDownloadButtonSensitivity() - - if self.refreshSampleDownloadFolder: - cmd_line("\n" + _("Download folder preferences were changed.")) - for w in workers.getScanningWorkers(): - if not w.scanResultsStaleDownloadFolder: - w.scanResultsStaleDownloadFolder = True - self.noAfterScanRefreshSampleDownloadFolders += 1 - - self.selection_vbox.selection_treeview.refreshSampleDownloadFolders() - self.refreshSampleDownloadFolder = False + return valid - def regenerateScannedDevices(self, thread_id): + def same_file_system(self, file1, file2): + """Returns True if the files / diretories are on the same file system """ - Regenerate the filenames / subfolders / download folders for this thread + f1 = gio.File(file1) + f2 = gio.File(file2) + f1_info = f1.query_info(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) + f1_id = f1_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) + f2_info = f2.query_info(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) + f2_id = f2_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILESYSTEM) + return f1_id == f2_id - The user must have adjusted their preferences as the device was being scanned + + def same_file(self, file1, file2): + """Returns True if the files / directories are the same """ + f1 = gio.File(file1) + f2 = gio.File(file2) - if self.noAfterScanRefreshSampleDownloadFolders: - # no point updating it if we're going to update it in the - # refresh of sample names and subfolders anway! - if not self.noAfterScanRefreshGeneratedSampleSubfolderAndName: - self.selection_vbox.selection_treeview.refreshSampleDownloadFolders(thread_id) - self.noAfterScanRefreshSampleDownloadFolders -= 1 - - if self.noAfterScanRefreshGeneratedSampleSubfolderAndName: - self.selection_vbox.selection_treeview.refreshGeneratedSampleSubfolderAndName(thread_id) - self.noAfterScanRefreshGeneratedSampleSubfolderAndName -= 1 - - + 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 on_error_eventbox_button_press_event(self, widget, event): - self.prefs.show_log_dialog = True - log_dialog.widget.show() - -class VMonitor: - """ Transistion to gvfs from gnomevfs""" - def __init__(self, app): - self.app = app - if using_gio: - self.vmonitor = gio.volume_monitor_get() - self.vmonitor.connect("mount-added", self.app.on_volume_mounted) - self.vmonitor.connect("mount-removed", self.app.on_volume_unmounted) - else: - self.vmonitor = gnomevfs.VolumeMonitor() - self.vmonitor.connect("volume-mounted", self.app.on_volume_mounted) - self.vmonitor.connect("volume-unmounted", self.app.on_volume_unmounted) - + def is_valid_download_dir(self, path): + """ + Checks the following conditions: + Does the directory exist? + Is it writable? + """ + valid = False + try: + d = gio.File(path) + if not d.query_exists(cancellable=None): + logger.error("Download directory does not exist: %s", path) + else: + file_attributes = "standard::type,access::can-read,access::can-write" + file_info = d.query_filesystem_info(file_attributes) + file_type = file_info.get_file_type() + + if file_type != gio.FILE_TYPE_DIRECTORY and file_type != gio.FILE_TYPE_UNKNOWN: + logger.error("%s is an invalid directory", path) + else: + # is the directory writable? + try: + temp_dir = tempfile.mkdtemp(prefix="rpd-tmp", dir=path) + valid = True + except: + logger.error("%s is not writable", path) + else: + f = gio.File(temp_dir) + f.delete(cancellable=None) - def get_mounts(self): - if using_gio: - return self.vmonitor.get_mounts() - else: - return self.vmonitor.get_mounted_volumes() + except gio.Error, inst: + logger.error("Error checking download directory %s", path) + logger.error(inst) -class Volume: - """ Transistion to gvfs from gnomevfs""" - def __init__(self, volume): - self.volume = volume - - def get_name(self, limit=config.MAX_LENGTH_DEVICE_NAME): - if using_gio: - v = self.volume.get_name() - else: - v = self.volume.get_display_name() - - if limit: - if len(v) > limit: - v = v[:limit] + '...' - return v + return valid + + + + # # # + # Process results and management + # # # - def get_path(self, avoid_gnomeVFS_bug = False): - if using_gio: - path = self.volume.get_root().get_path() - else: - uri = self.volume.get_activation_uri() - path = None - if avoid_gnomeVFS_bug: - # ugly hack to work around bug where gnomevfs.get_local_path_from_uri(uri) causes a crash - mediaLocation = "file://" + config.MEDIA_LOCATION - if uri.find(mediaLocation) == 0: - path = gnomevfs.get_local_path_from_uri(uri) - else: - path = gnomevfs.get_local_path_from_uri(uri) - return path + 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. + """ - def get_icon_pixbuf(self, size): - """ returns icon for the volume, or None if not available""" + self.batch_size = 10 + self.batch_size_MB = 2 - return common.get_icon_pixbuf(using_gio, self.volume.get_icon(), size) + sequence_values = (self.downloads_today_value, + self.downloads_today_date_value, + self.day_start_value, + self.refresh_downloads_today_value, + self.stored_sequence_value, + self.uses_stored_sequence_no_value, + self.uses_session_sequece_no_value, + self.uses_sequence_letter_value) + + self.subfolder_file_manager = SubfolderFileManager( + self.subfolder_file_results, + sequence_values) - def unmount(self, callback): - self.volume.unmount(callback) - -class DownloadStats: - def __init__(self): - self.clear() - def adjust(self, size, noImagesDownloaded, noVideosDownloaded, noImagesSkipped, noVideosSkipped, noWarnings, noErrors): - self.downloadSize += size - self.noImagesDownloaded += noImagesDownloaded - self.noVideosDownloaded += noVideosDownloaded - self.noImagesSkipped += noImagesSkipped - self.noVideosSkipped += noVideosSkipped - self.noWarnings += noWarnings - self.noErrors += noErrors - - def clear(self): - self.noImagesDownloaded = self.noVideosDownloaded = self.noImagesSkipped = self.noVideosSkipped = 0 - self.downloadSize = 0 - self.noWarnings = self.noErrors = 0 - -class DownloadedFiles: - def __init__(self): - self.images = {} - - def add_download(self, name, extension, date_time, sub_seconds, sequence_number_used): - if name not in self.images: - self.images[name] = ([extension], date_time, sub_seconds, sequence_number_used) - else: - if extension not in self.images[name][0]: - self.images[name][0].append(extension) - + self.generate_folder = False + self.scan_manager = ScanManager(self.scan_results, self.batch_size, + self.generate_folder, self.device_collection.add_device) + self.copy_files_manager = CopyFilesManager(self.copy_files_results, + self.batch_size_MB) - 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. + def scan_results(self, source, condition): + """ + Receive results from scan processes + """ + connection = self.scan_manager.get_pipe(source) - 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 images detected with the same filenames, but taken at different times - Returns 1 and a sequence number of None if no match""" + conn_type, data = connection.recv() - if name in self.images: - if self.images[name][1] == date_time and self.images[name][2] == sub_seconds: - if extension in self.images[name][0]: - return (-1, self.images[name][3]) - else: - return (0, self.images[name][3]) + if conn_type == rpdmp.CONN_COMPLETE: + connection.close() + size, file_type_counter, scan_pid = data + size = format_size_for_user(bytes=size) + results_summary, file_types_present = file_type_counter.summarize_file_count() + self.file_types_present_by_scan_pid[scan_pid] = file_types_present + logger.info('Found %s' % results_summary) + logger.info('Files total %s' % size) + self.device_collection.update_device(scan_pid, size) + self.device_collection.update_progress(scan_pid, 0.0, results_summary, 0) + self.testing_auto_exit_trip_counter += 1 + if self.testing_auto_exit_trip_counter == self.testing_auto_exit_trip and self.testing_auto_exit: + self.on_rapidapp_destroy(self.rapidapp) else: - return (-99, None) - return (1, None) - - def extExifDateTime(self, name): - """Returns first extension, exif date time and subseconds data for the already downloaded image""" - return (self.images[name][0][0], self.images[name][1], self.images[name][2]) - -class TimeForDownload: - # used to store variables, see below - pass - -class TimeRemaining: - gap = 2 - def __init__(self): - self.clear() - - def set(self, w, size): - t = TimeForDownload() - t.timeRemaining = None - t.size = size - t.downloaded = 0 - t.sizeMark = 0 - t.timeMark = time.time() - self.times[w] = t - - def update(self, w, size): - if w in self.times: - self.times[w].downloaded += size - now = time.time() - tm = self.times[w].timeMark - amtTime = now - tm - if amtTime > self.gap: - self.times[w].timeMark = now - amtDownloaded = self.times[w].downloaded - self.times[w].sizeMark - self.times[w].sizeMark = self.times[w].downloaded - timefraction = amtDownloaded / float(amtTime) - amtToDownload = float(self.times[w].size) - self.times[w].downloaded - if timefraction: - self.times[w].timeRemaining = amtToDownload / timefraction - - def _timeEstimates(self): - for t in self.times: - yield self.times[t].timeRemaining + if not self.testing_auto_exit: + self.download_progressbar.set_text(_("Thumbnails")) + self.thumbnails.generate_thumbnails(scan_pid) + self.set_download_action_sensitivity() + self.set_thumbnail_sort() - def timeRemaining(self): - return max(self._timeEstimates()) - - def setTimeMark(self, w): - if w in self.times: - self.times[w].timeMark = time.time() + # signal that no more data is coming, finishing io watch for this pipe + return False + else: + if len(data) > self.batch_size: + logger.error("incoming pipe length is %s" % len(data)) + else: + for rpd_file in data: + self.thumbnails.add_file(rpd_file) - def clear(self): - self.times = {} + # must return True for this method to be called again + return True - def remove(self, w): - if w in self.times: - del self.times[w] -def programStatus(): - print _("Goodbye") + @dbus.service.method (config.DBUS_NAME, + in_signature='', out_signature='b') + def is_running (self): + return self.running + + @dbus.service.method (config.DBUS_NAME, + in_signature='', out_signature='') + def start (self): + if self.is_running(): + self.rapidapp.present() + else: + self.running = True + gtk.main() -def start (): - global is_beta - is_beta = config.version.find('~b') > 0 +def start(): + + is_beta = config.version.find('~') > 0 - parser = OptionParser(version= "%%prog %s" % config.version) + parser = OptionParser(version= "%%prog %s" % utilities.human_readable_version(config.version)) parser.set_defaults(verbose=is_beta, extensions=False) # Translators: this text is displayed to the user when they request information on the command line options. # The text %default should not be modified or left out. @@ -6468,19 +2547,18 @@ def start (): parser.add_option("-e", "--extensions", action="store_true", dest="extensions", help=_("list photo and video file extensions the program recognizes and exit")) parser.add_option("--reset-settings", action="store_true", dest="reset", help=_("reset all program settings and preferences and exit")) (options, args) = parser.parse_args() - global verbose - verbose = options.verbose - global debug_info - debug_info = options.debug - if debug_info: - verbose = True + if options.debug: + logging_level = logging.DEBUG + elif options.verbose: + logging_level = logging.INFO + else: + logging_level = logging.ERROR - if verbose: - atexit.register(programStatus) - + logger.setLevel(logging_level) + if options.extensions: - extensions = ((metadata.RAW_FILE_EXTENSIONS + metadata.NON_RAW_IMAGE_FILE_EXTENSIONS, _("Photos:")), (videometadata.VIDEO_FILE_EXTENSIONS, _("Videos:"))) + extensions = ((rpdfile.RAW_FILE_EXTENSIONS + rpdfile.NON_RAW_IMAGE_FILE_EXTENSIONS, _("Photos:")), (rpdfile.VIDEO_FILE_EXTENSIONS, _("Videos:"))) for exts, file_type in extensions: v = '' for e in exts[:-1]: @@ -6496,43 +2574,25 @@ def start (): print _("All settings and preferences have been reset") sys.exit(0) - cmd_line(_("Rapid Photo Downloader") + " %s" % config.version) - cmd_line(_("Using") + " pyexiv2 " + metadata.version_info()) - cmd_line(_("Using") + " exiv2 " + metadata.exiv2_version_info()) + logger.info("Rapid Photo Downloader %s", utilities.human_readable_version(config.version)) + logger.info("Using pyexiv2 %s", metadataphoto.pyexiv2_version_info()) + logger.info("Using exiv2 %s", metadataphoto.exiv2_version_info()) if DOWNLOAD_VIDEO: - cmd_line(_("Using") + " hachoir " + videometadata.version_info()) - else: - cmd_line(_("\n" + "Video downloading functionality disabled.\nTo download videos, please install the hachoir metadata and kaa metadata packages for python.") + "\n") - - if using_gio: - cmd_line(_("Using") + " GIO") - gobject.threads_init() + logger.info("Using hachoir %s", metadatavideo.version_info()) else: - # Which volume management code is being used (GIO or GnomeVFS) - cmd_line(_("Using") + " GnomeVFS") - gdk.threads_init() - - - - display_queue.open("rw") - tube.tube_add_watch(display_queue, updateDisplay) + logger.info(_("Video downloading functionality disabled.\nTo download videos, please install the hachoir metadata and kaa metadata packages for python.")) - gdk.threads_enter() - - # run only a single instance of the application bus = dbus.SessionBus () request = bus.request_name (config.DBUS_NAME, dbus.bus.NAME_FLAG_DO_NOT_QUEUE) - if request != dbus.bus.REQUEST_NAME_REPLY_EXISTS: - app = RapidApp (bus, '/', config.DBUS_NAME) + if request != dbus.bus.REQUEST_NAME_REPLY_EXISTS: + app = RapidApp(bus, '/', config.DBUS_NAME) else: # this application is already running - print _("%s is already running") % PROGRAM_NAME + print "Rapid Photo Downloader is already running" object = bus.get_object (config.DBUS_NAME, "/") app = dbus.Interface (object, config.DBUS_NAME) - app.start() - - gdk.threads_leave() + app.start() if __name__ == "__main__": start() diff --git a/rapid/renamesubfolderprefs.py b/rapid/renamesubfolderprefs.py deleted file mode 100644 index d46a8c1..0000000 --- a/rapid/renamesubfolderprefs.py +++ /dev/null @@ -1,1693 +0,0 @@ -#!/usr/bin/python -# -*- coding: latin1 -*- - -### Copyright (C) 2007, 2008, 2009, 2010 Damon Lynch - -### This program is free software; you can redistribute it and/or modify -### it under the terms of the GNU General Public License as published by -### the Free Software Foundation; either version 2 of the License, or -### (at your option) any later version. - -### This program is distributed in the hope that it will be useful, -### but WITHOUT ANY WARRANTY; without even the implied warranty of -### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -### GNU General Public License for more details. - -### You should have received a copy of the GNU General Public License -### along with this program; if not, write to the Free Software -### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -""" Define and test preferences for use in PlusMinus tables. - -These are displayed to the user as a series of rows in the user -preferences dialog window. - -Preferences for subfolders and image renaming are defined below -in dictionaries and lists. This makes it easier for checking validity and -creating combo boxes. - -There are 3 levels: 0, 1 and 2, which specify the depth of the pref value. -Level 0 is the topmost level, and corresponds to the first entry in the -row of preferences the user sees in the preferences dialog window. - -Custom exceptions are defined to handle invalid preferences. - -The user's actual preferences, on the other hand, are stored in flat lists. -Each list has members which are a multiple of 3 in length. -Each group of 3 members is equal to one line of preferences in the plus minus -table. -""" -#needed for python 2.5, unneeded for python 2.6 -from __future__ import with_statement - -import string - -import os -import re -import sys - -import gtk.gdk as gdk - -try: - import pygtk - pygtk.require("2.0") -except: - pass -try: - import gtk -except: - sys.exit(1) - -from common import Configi18n -global _ -_ = Configi18n._ - -import datetime - -import config - -from common import pythonifyVersion -import problemnotification as pn - - -# Special key in each dictionary which specifies the order of elements. -# It is very important to have a consistent and rational order when displaying -# these prefs to the user, and dictionaries are unsorted. - -ORDER_KEY = "__order__" - -# PLEASE NOTE: these values are duplicated in a dummy class whose function -# is to have them put into the translation template. If you change the values below -# then you MUST change the value in class i18TranslateMeThanks as well!! - -# *** Level 0 -DATE_TIME = 'Date time' -TEXT = 'Text' -FILENAME = 'Filename' -METADATA = 'Metadata' -SEQUENCES = 'Sequences' -JOB_CODE = 'Job code' - -SEPARATOR = os.sep - -# *** Level 1 - -# Date time -IMAGE_DATE = 'Image date' -TODAY = 'Today' -YESTERDAY = 'Yesterday' -VIDEO_DATE = 'Video date' -DOWNLOAD_TIME = 'Download time' - -# File name -NAME_EXTENSION = 'Name + extension' -NAME = 'Name' -EXTENSION = 'Extension' -IMAGE_NUMBER = 'Image number' -VIDEO_NUMBER = 'Video number' - -# Metadata -APERTURE = 'Aperture' -ISO = 'ISO' -EXPOSURE_TIME = 'Exposure time' -FOCAL_LENGTH = 'Focal length' -CAMERA_MAKE = 'Camera make' -CAMERA_MODEL = 'Camera model' -SHORT_CAMERA_MODEL = 'Short camera model' -SHORT_CAMERA_MODEL_HYPHEN = 'Hyphenated short camera model' -SERIAL_NUMBER = 'Serial number' -SHUTTER_COUNT = 'Shutter count' -OWNER_NAME = 'Owner name' - -# Video metadata -CODEC = 'Codec' -WIDTH = 'Width' -HEIGHT = 'Height' -FPS = 'Frames Per Second' -LENGTH = 'Length' - -#Image sequences -DOWNLOAD_SEQ_NUMBER = 'Downloads today' -SESSION_SEQ_NUMBER = 'Session number' -SUBFOLDER_SEQ_NUMBER = 'Subfolder number' -STORED_SEQ_NUMBER = 'Stored number' - -SEQUENCE_LETTER = 'Sequence letter' - - - -# *** Level 2 - -# Image number -IMAGE_NUMBER_ALL = 'All digits' -IMAGE_NUMBER_1 = 'Last digit' -IMAGE_NUMBER_2 = 'Last 2 digits' -IMAGE_NUMBER_3 = 'Last 3 digits' -IMAGE_NUMBER_4 = 'Last 4 digits' - - -# Case -ORIGINAL_CASE = "Original Case" -UPPERCASE = "UPPERCASE" -LOWERCASE = "lowercase" - -# Sequence number -SEQUENCE_NUMBER_1 = "One digit" -SEQUENCE_NUMBER_2 = "Two digits" -SEQUENCE_NUMBER_3 = "Three digits" -SEQUENCE_NUMBER_4 = "Four digits" -SEQUENCE_NUMBER_5 = "Five digits" -SEQUENCE_NUMBER_6 = "Six digits" -SEQUENCE_NUMBER_7 = "Seven digits" - - -# Now, define dictionaries and lists of valid combinations of preferences. - -# Level 2 - -# Date - -SUBSECONDS = 'Subseconds' - -# ****** NOTE 1: if changing LIST_DATE_TIME_L2, you MUST update the default subfolder preference below ***** -# ****** NOTE 2: if changing LIST_DATE_TIME_L2, you MUST update DATE_TIME_CONVERT below ***** -LIST_DATE_TIME_L2 = ['YYYYMMDD', 'YYYY-MM-DD','YYMMDD', 'YY-MM-DD', - 'MMDDYYYY', 'MMDDYY', 'MMDD', - 'DDMMYYYY', 'DDMMYY', 'YYYY', 'YY', - 'MM', 'DD', - 'HHMMSS', 'HHMM', 'HH-MM-SS', 'HH-MM', 'HH', 'MM (minutes)', 'SS'] - - -LIST_IMAGE_DATE_TIME_L2 = LIST_DATE_TIME_L2 + [SUBSECONDS] - -DEFAULT_SUBFOLDER_PREFS = [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[9], '/', '', '', DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0]] -DEFAULT_VIDEO_SUBFOLDER_PREFS = [DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[9], '/', '', '', DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0]] - -class i18TranslateMeThanks: - """ this class is never used in actual running code - It's purpose is to have these values inserted into the program's i18n template file - - """ - def __init__(self): - _('Date time') - _('Text') - _('Filename') - _('Metadata') - _('Sequences') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode - _('Job code') - _('Image date') - _('Video date') - _('Today') - _('Yesterday') - # Translators: Download time is the time and date that the download started (when the user clicked the Download button) - _('Download time') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Name + extension') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Name') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Extension') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Image number') - _('Video number') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Aperture') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('ISO') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Exposure time') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Focal length') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Camera make') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Camera model') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Short camera model') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Hyphenated short camera model') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Serial number') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Shutter count') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata - _('Owner name') - _('Codec') - _('Width') - _('Height') - _('Length') - _('Frames Per Second') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers - _('Downloads today') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers - _('Session number') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers - _('Subfolder number') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers - _('Stored number') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequenceletters - _('Sequence letter') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('All digits') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Last digit') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Last 2 digits') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Last 3 digits') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename - _('Last 4 digits') - # Translators: please not the capitalization of this text, and keep it the same if your language features capitalization - _("Original Case") - # Translators: please not the capitalization of this text, and keep it the same if your language features capitalization - _("UPPERCASE") - # Translators: please not the capitalization of this text, and keep it the same if your language features capitalization - _("lowercase") - _("One digit") - _("Two digits") - _("Three digits") - _("Four digits") - _("Five digits") - _("Six digits") - _("Seven digits") - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('Subseconds') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('YYYYMMDD') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('YYYY-MM-DD') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('YYMMDD') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('YY-MM-DD') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('MMDDYYYY') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('MMDDYY') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('MMDD') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('DDMMYYYY') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('DDMMYY') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('YYYY') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('YY') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('MM') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('DD') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('HHMMSS') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('HHMM') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('HH-MM-SS') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('HH-MM') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('HH') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('MM (minutes)') - # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime - _('SS') - - -# Convenience values for python datetime conversion using values in -# LIST_DATE_TIME_L2. Obviously the two must remain synchronized. - -DATE_TIME_CONVERT = ['%Y%m%d', '%Y-%m-%d','%y%m%d', '%y-%m-%d', - '%m%d%Y', '%m%d%y', '%m%d', - '%d%m%Y', '%d%m%y', '%Y', '%y', - '%m', '%d', - '%H%M%S', '%H%M', '%H-%M-%S', '%H-%M', - '%H', '%M', '%S'] - - -LIST_IMAGE_NUMBER_L2 = [IMAGE_NUMBER_ALL, IMAGE_NUMBER_1, IMAGE_NUMBER_2, - IMAGE_NUMBER_3, IMAGE_NUMBER_4] - - -LIST_CASE_L2 = [ORIGINAL_CASE, UPPERCASE, LOWERCASE] - -LIST_SEQUENCE_LETTER_L2 = [ - UPPERCASE, - LOWERCASE - ] - - - -LIST_SEQUENCE_NUMBERS_L2 = [ - SEQUENCE_NUMBER_1, - SEQUENCE_NUMBER_2, - SEQUENCE_NUMBER_3, - SEQUENCE_NUMBER_4, - SEQUENCE_NUMBER_5, - SEQUENCE_NUMBER_6, - SEQUENCE_NUMBER_7, - ] - - - -LIST_SHUTTER_COUNT_L2 = [ - SEQUENCE_NUMBER_3, - SEQUENCE_NUMBER_4, - SEQUENCE_NUMBER_5, - SEQUENCE_NUMBER_6, - ] - -# Level 1 -LIST_DATE_TIME_L1 = [IMAGE_DATE, TODAY, YESTERDAY, DOWNLOAD_TIME] -LIST_VIDEO_DATE_TIME_L1 = [VIDEO_DATE, TODAY, YESTERDAY, DOWNLOAD_TIME] - -DICT_DATE_TIME_L1 = { - IMAGE_DATE: LIST_IMAGE_DATE_TIME_L2, - TODAY: LIST_DATE_TIME_L2, - YESTERDAY: LIST_DATE_TIME_L2, - DOWNLOAD_TIME: LIST_DATE_TIME_L2, - ORDER_KEY: LIST_DATE_TIME_L1 - } - -VIDEO_DICT_DATE_TIME_L1 = { - VIDEO_DATE: LIST_IMAGE_DATE_TIME_L2, - TODAY: LIST_DATE_TIME_L2, - YESTERDAY: LIST_DATE_TIME_L2, - DOWNLOAD_TIME: LIST_DATE_TIME_L2, - ORDER_KEY: LIST_VIDEO_DATE_TIME_L1 - } - - -LIST_FILENAME_L1 = [NAME_EXTENSION, NAME, EXTENSION, IMAGE_NUMBER] - -DICT_FILENAME_L1 = { - NAME_EXTENSION: LIST_CASE_L2, - NAME: LIST_CASE_L2, - EXTENSION: LIST_CASE_L2, - IMAGE_NUMBER: LIST_IMAGE_NUMBER_L2, - ORDER_KEY: LIST_FILENAME_L1 - } - -LIST_VIDEO_FILENAME_L1 = [NAME_EXTENSION, NAME, EXTENSION, VIDEO_NUMBER] - -DICT_VIDEO_FILENAME_L1 = { - NAME_EXTENSION: LIST_CASE_L2, - NAME: LIST_CASE_L2, - EXTENSION: LIST_CASE_L2, - VIDEO_NUMBER: LIST_IMAGE_NUMBER_L2, - ORDER_KEY: LIST_VIDEO_FILENAME_L1 - } - - -LIST_SUBFOLDER_FILENAME_L1 = [EXTENSION] - -DICT_SUBFOLDER_FILENAME_L1 = { - EXTENSION: LIST_CASE_L2, - ORDER_KEY: LIST_SUBFOLDER_FILENAME_L1 -} - -LIST_METADATA_L1 = [APERTURE, ISO, EXPOSURE_TIME, FOCAL_LENGTH, - CAMERA_MAKE, CAMERA_MODEL, - SHORT_CAMERA_MODEL, - SHORT_CAMERA_MODEL_HYPHEN, - SERIAL_NUMBER, - SHUTTER_COUNT, - OWNER_NAME] - -LIST_VIDEO_METADATA_L1 = [CODEC, WIDTH, HEIGHT, LENGTH, FPS] - -DICT_METADATA_L1 = { - APERTURE: None, - ISO: None, - EXPOSURE_TIME: None, - FOCAL_LENGTH: None, - CAMERA_MAKE: LIST_CASE_L2, - CAMERA_MODEL: LIST_CASE_L2, - SHORT_CAMERA_MODEL: LIST_CASE_L2, - SHORT_CAMERA_MODEL_HYPHEN: LIST_CASE_L2, - SERIAL_NUMBER: None, - SHUTTER_COUNT: LIST_SHUTTER_COUNT_L2, - OWNER_NAME: LIST_CASE_L2, - ORDER_KEY: LIST_METADATA_L1 - } - -DICT_VIDEO_METADATA_L1 = { - CODEC: LIST_CASE_L2, - WIDTH: None, - HEIGHT: None, - LENGTH: None, - FPS: None, - ORDER_KEY: LIST_VIDEO_METADATA_L1 - } - -LIST_SEQUENCE_L1 = [ - DOWNLOAD_SEQ_NUMBER, - STORED_SEQ_NUMBER, - SESSION_SEQ_NUMBER, - SEQUENCE_LETTER - ] - -DICT_SEQUENCE_L1 = { - DOWNLOAD_SEQ_NUMBER: LIST_SEQUENCE_NUMBERS_L2, - STORED_SEQ_NUMBER: LIST_SEQUENCE_NUMBERS_L2, - SESSION_SEQ_NUMBER: LIST_SEQUENCE_NUMBERS_L2, - SEQUENCE_LETTER: LIST_SEQUENCE_LETTER_L2, - ORDER_KEY: LIST_SEQUENCE_L1 - } - - -# Level 0 - - -LIST_IMAGE_RENAME_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, - SEQUENCES, JOB_CODE] - -LIST_VIDEO_RENAME_L0 = LIST_IMAGE_RENAME_L0 - - -DICT_IMAGE_RENAME_L0 = { - DATE_TIME: DICT_DATE_TIME_L1, - TEXT: None, - FILENAME: DICT_FILENAME_L1, - METADATA: DICT_METADATA_L1, - SEQUENCES: DICT_SEQUENCE_L1, - JOB_CODE: None, - ORDER_KEY: LIST_IMAGE_RENAME_L0 - } - -DICT_VIDEO_RENAME_L0 = { - DATE_TIME: VIDEO_DICT_DATE_TIME_L1, - TEXT: None, - FILENAME: DICT_VIDEO_FILENAME_L1, - METADATA: DICT_VIDEO_METADATA_L1, - SEQUENCES: DICT_SEQUENCE_L1, - JOB_CODE: None, - ORDER_KEY: LIST_VIDEO_RENAME_L0 - } - -LIST_SUBFOLDER_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, JOB_CODE, SEPARATOR] - -DICT_SUBFOLDER_L0 = { - DATE_TIME: DICT_DATE_TIME_L1, - TEXT: None, - FILENAME: DICT_SUBFOLDER_FILENAME_L1, - METADATA: DICT_METADATA_L1, - JOB_CODE: None, - SEPARATOR: None, - ORDER_KEY: LIST_SUBFOLDER_L0 - } - -LIST_VIDEO_SUBFOLDER_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, JOB_CODE, SEPARATOR] - -DICT_VIDEO_SUBFOLDER_L0 = { - DATE_TIME: VIDEO_DICT_DATE_TIME_L1, - TEXT: None, - FILENAME: DICT_SUBFOLDER_FILENAME_L1, - METADATA: DICT_VIDEO_METADATA_L1, - JOB_CODE: None, - SEPARATOR: None, - ORDER_KEY: LIST_VIDEO_SUBFOLDER_L0 - } - -# preference elements that require metadata -# note there is no need to specify lower level elements if a higher level -# element is necessary for them to be present to begin with -METADATA_ELEMENTS = [METADATA, IMAGE_DATE] - -# preference elements that are sequence numbers or letters -SEQUENCE_ELEMENTS = [ - DOWNLOAD_SEQ_NUMBER, - SESSION_SEQ_NUMBER, - SUBFOLDER_SEQ_NUMBER, - STORED_SEQ_NUMBER, - SEQUENCE_LETTER] - -# preference elements that do not require metadata and are not fixed -# as above, there is no need to specify lower level elements if a higher level -# element is necessary for them to be present to begin with -DYNAMIC_NON_METADATA_ELEMENTS = [ - TODAY, YESTERDAY, - FILENAME] + SEQUENCE_ELEMENTS - - - -#the following is what the preferences looked in older versions of the program -#they are here for reference, and for checking the validity of preferences - -USER_INPUT = 'User' - -DOWNLOAD_SEQ_NUMBER_V_0_0_8_B7 = 'Downloads today' -SESSION_SEQ_NUMBER_V_0_0_8_B7 = 'Session sequence number' -SUBFOLDER_SEQ_NUMBER_V_0_0_8_B7 = 'Subfolder sequence number' -STORED_SEQ_NUMBER_V_0_0_8_B7 = 'Stored sequence number' -SEQUENCE_LETTER_V_0_0_8_B7 = 'Sequence letter' - -LIST_SEQUENCE_NUMBERS_L1_L2_V_0_0_8_B7 = [ - SEQUENCE_NUMBER_1, - SEQUENCE_NUMBER_2, - SEQUENCE_NUMBER_3, - SEQUENCE_NUMBER_4, - SEQUENCE_NUMBER_5, - SEQUENCE_NUMBER_6, - ] - -DICT_SEQUENCE_NUMBERS_L1_L2_V_0_0_8_B7 = { - SEQUENCE_NUMBER_1: None, - SEQUENCE_NUMBER_2: None, - SEQUENCE_NUMBER_3: None, - SEQUENCE_NUMBER_4: None, - SEQUENCE_NUMBER_5: None, - SEQUENCE_NUMBER_6: None, - ORDER_KEY: LIST_SEQUENCE_NUMBERS_L1_L2_V_0_0_8_B7 - } - -LIST_SEQUENCE_L1_V_0_0_8_B7 = [USER_INPUT] - -DICT_SEQUENCE_L1_V_0_0_8_B7 = { - USER_INPUT: DICT_SEQUENCE_NUMBERS_L1_L2_V_0_0_8_B7, - ORDER_KEY: LIST_SEQUENCE_L1_V_0_0_8_B7 - } - -LIST_SEQUENCE_LETTER_L1_L1_V_0_0_8_B7 = [ - UPPERCASE, - LOWERCASE - ] - -DICT_SEQUENCE_LETTER_L1_V_0_0_8_B7 = { - UPPERCASE: None, - LOWERCASE: None, - ORDER_KEY: LIST_SEQUENCE_LETTER_L1_L1_V_0_0_8_B7 - } - -LIST_IMAGE_RENAME_L0_V_0_0_8_B7 = [DATE_TIME, TEXT, FILENAME, METADATA, - DOWNLOAD_SEQ_NUMBER_V_0_0_8_B7, - SESSION_SEQ_NUMBER_V_0_0_8_B7, - SEQUENCE_LETTER_V_0_0_8_B7] - -DICT_IMAGE_RENAME_L0_V_0_0_8_B7 = { - DATE_TIME: DICT_DATE_TIME_L1, - TEXT: None, - FILENAME: DICT_FILENAME_L1, - METADATA: DICT_METADATA_L1, - DOWNLOAD_SEQ_NUMBER_V_0_0_8_B7: None, - SESSION_SEQ_NUMBER_V_0_0_8_B7: None, - SEQUENCE_LETTER_V_0_0_8_B7: DICT_SEQUENCE_LETTER_L1_V_0_0_8_B7, - ORDER_KEY: LIST_IMAGE_RENAME_L0_V_0_0_8_B7 - } - -PREVIOUS_IMAGE_RENAME= { - '0.0.8~b7': DICT_IMAGE_RENAME_L0_V_0_0_8_B7, - } - - -# Functions to work with above data - -def _getPrevPrefs(oldDefs, currentDefs, previousVersion): - k = oldDefs.keys() - # if there were other defns, we'd need to figure out which one - # but currently, there are no others - # there will be in future, and this code wil be updated then - version_change = pythonifyVersion(k[0]) - if pythonifyVersion(previousVersion) <= version_change: - return oldDefs[k[0]] - else: - return currentDefs - -def _upgradePreferencesToCurrent(prefs, previousVersion): - """ checks to see if preferences should be upgraded - - returns True if they were upgraded, and the new prefs - - VERY IMPORTANT: the new prefs will be a new list, not an inplace - modification of the existing preferences! Otherwise, the check on - assignment in the prefs.py __setattr__ will not work as expected!! - """ - upgraded = False - # code to upgrade from <= 0.0.8~b7 to >= 0.0.8~b8 - p = [] - for i in range(0, len(prefs), 3): - if prefs[i] in [SEQUENCE_LETTER_V_0_0_8_B7, SESSION_SEQ_NUMBER_V_0_0_8_B7]: - upgraded = True - p.append(SEQUENCES) - if prefs[i] == SEQUENCE_LETTER_V_0_0_8_B7: - p.append(SEQUENCE_LETTER) - p.append(prefs[i+1]) - else: - p.append(SESSION_SEQ_NUMBER) - p.append(prefs[i+2]) - else: - p += prefs[i:i+3] - - assert(len(prefs)==len(p)) - return (upgraded, p) - - -def upgradePreferencesToCurrent(imageRenamePrefs, subfolderPrefs, previousVersion): - """Upgrades user preferences to current version - - returns True if the preferences were upgraded""" - - # only check image rename, for now.... - upgraded, imageRenamePrefs = _upgradePreferencesToCurrent(imageRenamePrefs, previousVersion) - return (upgraded, imageRenamePrefs , subfolderPrefs) - - -def usesJobCode(prefs): - """ Returns True if the preferences contain a job code, else returns False""" - for i in range(0, len(prefs), 3): - if prefs[i] == JOB_CODE: - return True - return False - -def checkPreferencesForValidity(imageRenamePrefs, subfolderPrefs, videoRenamePrefs, videoSubfolderPrefs, version=config.version): - """ - Checks preferences for validity (called at program startup) - - Returns true if the passed in preferences are valid, else returns False - """ - - if version == config.version: - try: - tests = ((imageRenamePrefs, ImageRenamePreferences), - (subfolderPrefs, SubfolderPreferences), - (videoRenamePrefs, VideoRenamePreferences), - (videoSubfolderPrefs, VideoSubfolderPreferences)) - for i, Prefs in tests: - p = Prefs(i, None) - p.checkPrefsForValidity() - except: - return False - return True - else: - defn = _getPrevPrefs(PREVIOUS_IMAGE_RENAME, DICT_IMAGE_RENAME_L0, version) - try: - checkPreferenceValid(defn, imageRenamePrefs) - checkPreferenceValid(DICT_SUBFOLDER_L0, subfolderPrefs) - checkPreferenceValid(DICT_VIDEO_SUBFOLDER_L0, videoSubfolderPrefs) - checkPreferenceValid(DICT_VIDEO_RENAME_L0, videoRenamePrefs) - except: - return False - return True - -def checkPreferenceValid(prefDefinition, prefs, modulo=3): - """ - Checks to see if prefs are valid according to definition. - - prefs is a list of preferences. - prefDefinition is a Dict specifying what is valid. - modulo is how many list elements are equivalent to one line of preferences. - - Returns True if prefs match with prefDefinition, - else raises appropriate error. - """ - - if (len(prefs) % modulo <> 0) or not prefs: - raise PrefLengthError(prefs) - else: - for i in range(0, len(prefs), modulo): - _checkPreferenceValid(prefDefinition, prefs[i:i+modulo]) - - return True - -def _checkPreferenceValid(prefDefinition, prefs): - - key = prefs[0] - value = prefs[1] - - - if prefDefinition.has_key(key): - - nextPrefDefinition = prefDefinition[key] - - if value == None: - # value should never be None, at any time - raise PrefValueInvalidError((None, nextPrefDefinition)) - - if nextPrefDefinition and not value: - raise PrefValueInvalidError((value, nextPrefDefinition)) - - if type(nextPrefDefinition) == type({}): - return _checkPreferenceValid(nextPrefDefinition, prefs[1:]) - else: - if type(nextPrefDefinition) == type([]): - result = value in nextPrefDefinition - if not result: - raise PrefValueInvalidError((value, nextPrefDefinition)) - return True - elif not nextPrefDefinition: - return True - else: - result = nextPrefDefinition == value - if not result: - raise PrefKeyValue((value, nextPrefDefinition)) - return True - else: - raise PrefKeyError((key, prefDefinition[ORDER_KEY])) - -def filterSubfolderPreferences(prefList): - """ - Filters out extraneous preference choices - """ - prefs_changed = False - continueCheck = True - while continueCheck and prefList: - continueCheck = False - if prefList[0] == SEPARATOR: - # Subfolder preferences should not start with a / - prefList = prefList[3:] - prefs_changed = True - continueCheck = True - elif prefList[-3] == SEPARATOR: - # Subfolder preferences should not end with a / - prefList = prefList[:-3] - continueCheck = True - prefs_changed = True - else: - for i in range(0, len(prefList) - 3, 3): - if prefList[i] == SEPARATOR and prefList[i+3] == SEPARATOR: - # Subfolder preferences should not contain two /s side by side - continueCheck = True - prefs_changed = True - # note we are messing with the contents of the pref list, - # must exit loop and try again - prefList = prefList[:i] + prefList[i+3:] - break - - return (prefs_changed, prefList) - - -class PrefError(Exception): - """ base class """ - def unpackList(self, l): - """ - Make the preferences presentable to the user - """ - - s = '' - for i in l: - if i <> ORDER_KEY: - s += "'" + i + "', " - return s[:-2] - - def __str__(self): - return self.msg - -class PrefKeyError(PrefError): - def __init__(self, error): - value = error[0] - expectedValues = self.unpackList(error[1]) - self.msg = _("Preference key '%(key)s' is invalid.\nExpected one of %(value)s") % { - 'key': value, 'value': expectedValues} - - -class PrefValueInvalidError(PrefKeyError): - def __init__(self, error): - value = error[0] - self.msg = _("Preference value '%(value)s' is invalid") % {'value': value} - -class PrefLengthError(PrefError): - def __init__(self, error): - self.msg = _("These preferences are not well formed:") + "\n %s" % self.unpackList(error) - -class PrefValueKeyComboError(PrefError): - def __init__(self, error): - self.msg = error - - -def convertDateForStrftime(dateTimeUserChoice): - try: - return DATE_TIME_CONVERT[LIST_DATE_TIME_L2.index(dateTimeUserChoice)] - except: - raise PrefValueInvalidError(dateTimeUserChoice) - - -class Comboi18n(gtk.ComboBox): - """ very simple i18n version of the venerable combo box - with one column displayed to the user. - - This combo box has two columns: - 1. the first contains the actual value and is invisible - 2. the second contains the translation of the first column, and this is what - the users sees - """ - def __init__(self): - liststore = gtk.ListStore(str, str) - gtk.ComboBox.__init__(self, liststore) - cell = gtk.CellRendererText() - self.pack_start(cell, True) - self.add_attribute(cell, 'text', 1) - - def append_text(self, text): - model = self.get_model() - model.append((text, _(text))) - - def get_active_text(self): - model = self.get_model() - active = self.get_active() - if active < 0: - return None - return model[active][0] - -class ImageRenamePreferences: - def __init__(self, prefList, parent, fileSequenceLock=None, sequences=None): - """ - Exception raised if preferences are invalid. - - This should be caught by calling class.""" - - self.parent = parent - self.prefList = prefList - - # use variables for determining sequence numbers - # there are two possibilities: - # 1. this code is being called while being run from within a copy photos process - # 2. it's being called from within the preferences dialog window - - self.fileSequenceLock = fileSequenceLock - self.sequences = sequences - - self.job_code = '' - - # derived classes will have their own definitions, do not overwrite - if not hasattr(self, "prefsDefnL0"): - self.prefsDefnL0 = DICT_IMAGE_RENAME_L0 - self.defaultPrefs = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE] - self.defaultRow = self.defaultPrefs - self.stripForwardSlash = True - self.L1DateCheck = IMAGE_DATE #used in _getDateComponent() - self.component = pn.FILENAME_COMPONENT - - - def initializeProblem(self, problem): - """ - Set the problem tracker used in name generation - """ - self.problem = problem - - def getProblems(self): - """ - Returns Problem class if there were problems, else returns None. - """ - if self.problem.has_problem(): - return self.problem - else: - return None - - def checkPrefsForValidity(self): - """ - Checks image preferences validity - """ - - return checkPreferenceValid(self.prefsDefnL0, self.prefList) - - def formatPreferencesForPrettyPrint(self): - """ returns a string useful for printing the preferences""" - - v = '' - - for i in range(0, len(self.prefList), 3): - if (self.prefList[i+1] or self.prefList[i+2]): - c = ':' - else: - c = '' - s = "%s%s " % (self.prefList[i], c) - - if self.prefList[i+1]: - s = "%s%s" % (s, self.prefList[i+1]) - if self.prefList[i+2]: - s = "%s (%s)" % (s, self.prefList[i+2]) - - v += s + "\n" - return v - - - def setJobCode(self, job_code): - self.job_code = job_code - - def setDownloadStartTime(self, download_start_time): - self.download_start_time = download_start_time - - def _getDateComponent(self): - """ - Returns portion of new image / 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.L1DateCheck: - if self.L2 == SUBSECONDS: - d = self.metadata.subSeconds() - if d == '00': - self.problem.add_problem(self.component, pn.MISSING_METADATA, _(self.L2)) - return '' - else: - return d - else: - d = self.metadata.dateTime(missing=None) - - elif self.L1 == TODAY: - d = datetime.datetime.now() - elif self.L1 == YESTERDAY: - delta = datetime.timedelta(days = 1) - d = datetime.datetime.now() - delta - elif self.L1 == DOWNLOAD_TIME: - d = self.download_start_time - else: - raise("Date options invalid") - - # step 2: handle a missing value - if not d: - if self.fallback_date: - try: - d = datetime.datetime.fromtimestamp(self.fallback_date) - except: - self.problem.add_problem(self.component, pn.INVALID_DATE_TIME, '') - return '' - else: - self.problem.add_problem(self.component, pn.MISSING_METADATA, _(self.L1)) - return '' - - try: - return d.strftime(convertDateForStrftime(self.L2)) - except: - self.problem.add_problem(self.component, pn.INVALID_DATE_TIME, d) - return '' - - def _getFilenameComponent(self): - """ - Returns portion of new image / subfolder name based on the file name - """ - - name, extension = os.path.splitext(self.existingFilename) - - if self.L1 == NAME_EXTENSION: - filename = self.existingFilename - elif self.L1 == NAME: - filename = name - elif self.L1 == EXTENSION: - if extension: - if not self.stripInitialPeriodFromExtension: - # keep the period / dot of the extension, so the user does not - # need to manually specify it - filename = extension - else: - # having the period when this is used as a part of a subfolder name - # is a bad idea! - filename = extension[1:] - else: - self.problem.add_problem(self.component, pn.MISSING_FILE_EXTENSION) - return "" - elif self.L1 == IMAGE_NUMBER or self.L1 == VIDEO_NUMBER: - n = re.search("(?P[0-9]+$)", name) - if not n: - self.problem.add_problem(self.component, pn.MISSING_IMAGE_NUMBER) - return '' - else: - image_number = n.group("image_number") - - if self.L2 == IMAGE_NUMBER_ALL: - filename = image_number - elif self.L2 == IMAGE_NUMBER_1: - filename = image_number[-1] - elif self.L2 == IMAGE_NUMBER_2: - filename = image_number[-2:] - elif self.L2 == IMAGE_NUMBER_3: - filename = image_number[-3:] - elif self.L2 == IMAGE_NUMBER_4: - filename = image_number[-4:] - else: - raise TypeError("Incorrect filename option") - - if self.L2 == UPPERCASE: - filename = filename.upper() - elif self.L2 == LOWERCASE: - filename = filename.lower() - - return filename - - def _getMetadataComponent(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.metadata.aperture() - elif self.L1 == ISO: - v = self.metadata.iso() - elif self.L1 == EXPOSURE_TIME: - v = self.metadata.exposureTime(alternativeFormat=True) - elif self.L1 == FOCAL_LENGTH: - v = self.metadata.focalLength() - elif self.L1 == CAMERA_MAKE: - v = self.metadata.cameraMake() - elif self.L1 == CAMERA_MODEL: - v = self.metadata.cameraModel() - elif self.L1 == SHORT_CAMERA_MODEL: - v = self.metadata.shortCameraModel() - elif self.L1 == SHORT_CAMERA_MODEL_HYPHEN: - v = self.metadata.shortCameraModel(includeCharacters = "\-") - elif self.L1 == SERIAL_NUMBER: - v = self.metadata.cameraSerial() - elif self.L1 == SHUTTER_COUNT: - v = self.metadata.shutterCount() - if v: - v = int(v) - padding = LIST_SHUTTER_COUNT_L2.index(self.L2) + 3 - formatter = '%0' + str(padding) + "i" - v = formatter % v - - elif self.L1 == OWNER_NAME: - v = self.metadata.ownerName() - else: - raise TypeError("Invalid metadata option specified") - if self.L1 in [CAMERA_MAKE, CAMERA_MODEL, SHORT_CAMERA_MODEL, - SHORT_CAMERA_MODEL_HYPHEN, OWNER_NAME]: - if self.L2 == UPPERCASE: - v = v.upper() - elif self.L2 == LOWERCASE: - v = v.lower() - if not v: - self.problem.add_problem(self.component, pn.MISSING_METADATA, _(self.L1)) - return v - - - def _formatSequenceNo(self, value, amountToPad): - padding = LIST_SEQUENCE_NUMBERS_L2.index(amountToPad) + 1 - formatter = '%0' + str(padding) + "i" - return formatter % value - - - def _calculateLetterSequence(self, sequence): - - def _letters(x): - """ - Adapted from algorithm at http://en.wikipedia.org/wiki/Hexavigesimal - """ - v = '' - while x > 25: - r = x % 26 - 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 _getSubfolderSequenceNo(self): - """ - Add a sequence number to the filename - - * Sequence numbering is per subfolder - * Assume the user might actually have a (perhaps odd) reason to have more - than one subfolder sequence number in the same file name - """ - - self.subfolderSeqNoInstanceInFilename += 1 - - if self.downloadSubfolder: - subfolder = self.downloadSubfolder + str(self.subfolderSeqNoInstanceInFilename) - else: - subfolder = "__subfolder__" + str(self.subfolderSeqNoInstanceInFilename) - - if self.fileSequenceLock: - with self.fileSequenceLock: - v = self.sequenceNos.calculate(subfolder) - v = self.formatSequenceNo(v, self.L1) - else: - v = self.sequenceNos.calculate(subfolder) - v = self.formatSequenceNo(v, self.L1) - - return v - - def _getSessionSequenceNo(self): - return self._formatSequenceNo(self.sequences.getSessionSequenceNoUsingCounter(self.sequenceCounter), self.L2) - - def _getDownloadsTodaySequenceNo(self): - return self._formatSequenceNo(self.sequences.getDownloadsTodayUsingCounter(self.sequenceCounter), self.L2) - - - def _getStoredSequenceNo(self): - return self._formatSequenceNo(self.sequences.getStoredSequenceNoUsingCounter(self.sequenceCounter), self.L2) - - def _getSequenceLetter(self): - return self._calculateLetterSequence(self.sequences.getSequenceLetterUsingCounter(self.sequenceCounter)) - - - def _getSequencesComponent(self): - if self.L1 == DOWNLOAD_SEQ_NUMBER: - return self._getDownloadsTodaySequenceNo() - elif self.L1 == SESSION_SEQ_NUMBER: - return self._getSessionSequenceNo() - elif self.L1 == SUBFOLDER_SEQ_NUMBER: - return self._getSubfolderSequenceNo() - elif self.L1 == STORED_SEQ_NUMBER: - return self._getStoredSequenceNo() - elif self.L1 == SEQUENCE_LETTER: - return self._getSequenceLetter() - - def _getComponent(self): - try: - if self.L0 == DATE_TIME: - return self._getDateComponent() - elif self.L0 == TEXT: - return self.L1 - elif self.L0 == FILENAME: - return self._getFilenameComponent() - elif self.L0 == METADATA: - return self._getMetadataComponent() - elif self.L0 == SEQUENCES: - return self._getSequencesComponent() - elif self.L0 == JOB_CODE: - return self.job_code - elif self.L0 == SEPARATOR: - return os.sep - except: - self.problem.add_problem(self.component, pn.ERROR_IN_GENERATION, _(self.L0)) - return '' - - def _getValuesFromList(self): - for i in range(0, len(self.prefList), 3): - yield (self.prefList[i], self.prefList[i+1], self.prefList[i+2]) - - - def _generateName(self, metadata, existingFilename, stripCharacters, subfolder, stripInitialPeriodFromExtension, sequence, fallback_date): - self.metadata = metadata - self.existingFilename = existingFilename - self.stripInitialPeriodFromExtension = stripInitialPeriodFromExtension - self.fallback_date = fallback_date - - name = '' - - #the subfolder in which the image will be downloaded to - self.downloadSubfolder = subfolder - - self.sequenceCounter = sequence - - for self.L0, self.L1, self.L2 in self._getValuesFromList(): - v = self._getComponent() - if v: - name += v - - if stripCharacters: - for c in r'\:*?"<>|': - name = name.replace(c, '') - - if self.stripForwardSlash: - name = name.replace('/', '') - - name = name.strip() - - return name - - def generateNameUsingPreferences(self, metadata, existingFilename=None, - stripCharacters = False, subfolder=None, - stripInitialPeriodFromExtension=False, - sequencesPreliminary = True, - sequence_to_use = None, - fallback_date = None): - """ - Generate a filename for the photo or video in string format based on user preferences. - - Returns the name in string format - - Any problems encountered during the generation of the name can be accessed - through the method getProblems() - """ - - if self.sequences: - if sequence_to_use is not None: - sequence = sequence_to_use - elif sequencesPreliminary: - sequence = self.sequences.getPrelimSequence() - else: - sequence = self.sequences.getFinalSequence() - else: - sequence = 0 - - return self._generateName(metadata, existingFilename, stripCharacters, subfolder, - stripInitialPeriodFromExtension, sequence, fallback_date) - - def generateNameSequencePossibilities(self, metadata, existingFilename, - stripCharacters=False, subfolder=None, - stripInitialPeriodFromExtension=False): - - """ Generates the possible image names using the sequence numbers / letter possibilities""" - - for sequence in self.sequences.getSequencePossibilities(): - yield self._generateName(metadata, existingFilename, stripCharacters, subfolder, - stripInitialPeriodFromExtension, sequence) - - def filterPreferences(self): - """ - Filters out extraneous preference choices - Expected to be implemented in derived classes when needed - """ - pass - - def needImageMetaDataToCreateUniqueName(self): - """ - Returns True if an image's metadata is essential to properly generate a unique image name - - Image names should be unique. Some images may not have metadata. If - only non-dynamic components make up the rest of an image name - (e.g. text specified by the user), then relying on metadata will likely - produce duplicate names. - - File extensions are not considered dynamic. - - This is NOT a general test to see if unique filenames can be generated. It is a test - to see if an image's metadata is needed. - """ - hasMD = hasDynamic = False - - for e in METADATA_ELEMENTS: - if e in self.prefList: - hasMD = True - break - - if hasMD: - for e in DYNAMIC_NON_METADATA_ELEMENTS: - if e in self.prefList: - if e == FILENAME and (NAME_EXTENSION in self.prefList or - NAME in self.prefList or - IMAGE_NUMBER in self.prefList): - hasDynamic = True - break - - if hasMD and not hasDynamic: - return True - else: - return False - - def usesSequenceElements(self): - """ Returns true if any sequence numbers or letters are used to generate the filename """ - - for e in SEQUENCE_ELEMENTS: - if e in self.prefList: - return True - - return False - - def usesTheSequenceElement(self, e): - """ Returns true if a stored sequence number is used to generate the filename """ - return e in self.prefList - - - def _createCombo(self, choices): - combobox = Comboi18n() - for text in choices: - combobox.append_text(text) - return combobox - - def getDefaultRow(self): - """ - returns a list of default widgets - """ - return self.getWidgetsBasedOnUserSelection(self.defaultRow) - - def _getPreferenceWidgets(self, prefDefinition, prefs, widgets): - key = prefs[0] - value = prefs[1] - - # supply a default value if the user has not yet chosen a value! - if not key: - key = prefDefinition[ORDER_KEY][0] - - if not key in prefDefinition: - raise PrefKeyError((key, prefDefinition.keys())) - - - list0 = prefDefinition[ORDER_KEY] - - # the first widget will always be a combo box - widget0 = self._createCombo(list0) - widget0.set_active(list0.index(key)) - - widgets.append(widget0) - - if key == TEXT: - widget1 = gtk.Entry() - widget1.set_text(value) - - widgets.append(widget1) - widgets.append(None) - return - elif key in [SEPARATOR, JOB_CODE]: - widgets.append(None) - widgets.append(None) - return - else: - nextPrefDefinition = prefDefinition[key] - if type(nextPrefDefinition) == type({}): - return self._getPreferenceWidgets(nextPrefDefinition, - prefs[1:], - widgets) - else: - if type(nextPrefDefinition) == type([]): - widget1 = self._createCombo(nextPrefDefinition) - if not value: - value = nextPrefDefinition[0] - try: - widget1.set_active(nextPrefDefinition.index(value)) - except: - raise PrefValueInvalidError((value, nextPrefDefinition)) - - widgets.append(widget1) - else: - widgets.append(None) - - def getWidgetsBasedOnPreferences(self): - """ - Yields a list of widgets and their callbacks based on the users preferences. - - This list is equivalent to one row of preferences when presented to the - user in the Plus Minus Table. - """ - - for L0, L1, L2 in self._getValuesFromList(): - prefs = [L0, L1, L2] - widgets = [] - self._getPreferenceWidgets(self.prefsDefnL0, prefs, widgets) - yield widgets - - - def getWidgetsBasedOnUserSelection(self, selection): - """ - Returns a list of widgets and their callbacks based on what the user has selected. - - Selection is the values the user has chosen thus far in comboboxes. - It determines the contents of the widgets returned. - It should be a list of three values, with None for values not chosen. - For values which are None, the first value in the preferences - definition is chosen. - - """ - widgets = [] - - self._getPreferenceWidgets(self.prefsDefnL0, selection, widgets) - return widgets - -def getVideoMetadataComponent(video): - """ - Returns portion of video / subfolder name based on the metadata - - This is outside of a class definition because of the inheritence - hierarchy. - """ - - problem = None - if video.L1 == CODEC: - v = video.metadata.codec() - elif video.L1 == WIDTH: - v = video.metadata.width() - elif video.L1 == HEIGHT: - v = video.metadata.height() - elif video.L1 == FPS: - v = video.metadata.framesPerSecond() - elif video.L1 == LENGTH: - v = video.metadata.length() - else: - raise TypeError("Invalid metadata option specified") - if video.L1 in [CODEC]: - if video.L2 == UPPERCASE: - v = v.upper() - elif video.L2 == LOWERCASE: - v = v.lower() - if not v: - video.problem.add_problem(video.component, pn.MISSING_METADATA, _(video.L1)) - return v - -class VideoRenamePreferences(ImageRenamePreferences): - def __init__(self, prefList, parent, fileSequenceLock=None, sequences=None): - self.prefsDefnL0 = DICT_VIDEO_RENAME_L0 - self.defaultPrefs = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE] - self.defaultRow = self.defaultPrefs - self.stripForwardSlash = True - self.L1DateCheck = VIDEO_DATE - self.component = pn.FILENAME_COMPONENT - ImageRenamePreferences.__init__(self, prefList, parent, fileSequenceLock, sequences) - - def _getMetadataComponent(self): - """ - Returns portion of video / subfolder name based on the metadata - - Note: date time metadata found in _getDateComponent() - """ - return getVideoMetadataComponent(self) - - -class SubfolderPreferences(ImageRenamePreferences): - def __init__(self, prefList, parent): - self.prefsDefnL0 = DICT_SUBFOLDER_L0 - self.defaultPrefs = DEFAULT_SUBFOLDER_PREFS - self.defaultRow = [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0]] - self.stripForwardSlash = False - self.L1DateCheck = IMAGE_DATE - self.component = pn.SUBFOLDER_COMPONENT - ImageRenamePreferences.__init__(self, prefList, parent) - - self.stripExtraneousWhiteSpace = re.compile(r'\s*%s\s*' % os.sep) - - def generateNameUsingPreferences(self, photo, existingFilename=None, - stripCharacters = False, fallback_date = None): - """ - Generate a filename for the photo in string format based on user prefs. - - Returns a tuple of two strings: - - the name - - any problems generating the name. If blank, there were no problems - """ - - subfolders = ImageRenamePreferences.generateNameUsingPreferences( - self, photo, - existingFilename, stripCharacters, - stripInitialPeriodFromExtension=True, - fallback_date=fallback_date) - # 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 stripCharacters: - subfolders = self.stripExtraneousWhiteSpace.sub(os.sep, subfolders) - - return subfolders - - def filterPreferences(self): - filtered, prefList = filterSubfolderPreferences(self.prefList) - if filtered: - self.prefList = prefList - - def needMetaDataToCreateUniqueName(self): - """ - Returns True if metadata is essential to properly generate subfolders - - This will be the case if the only components are metadata and separators - """ - - for e in self.prefList: - if (not e) and ((e not in METADATA_ELEMENTS) or (e <> SEPARATOR)): - return True - - return False - - - - - def checkPrefsForValidity(self): - """ - Checks subfolder preferences validity above and beyond image name checks. - - See parent method for full description. - - Subfolders have additional requirments to that of image names. - """ - v = ImageRenamePreferences.checkPrefsForValidity(self) - if v: - # peform additional checks: - # 1. do not start with a separator - # 2. do not end with a separator - # 3. do not have two separators in a row - # these three rules will ensure something else other than a - # separator is specified - L1s = [] - for i in range(0, len(self.prefList), 3): - L1s.append(self.prefList[i]) - - if L1s[0] == SEPARATOR: - raise PrefValueKeyComboError(_("Subfolder preferences should not start with a %s") % os.sep) - elif L1s[-1] == SEPARATOR: - raise PrefValueKeyComboError(_("Subfolder preferences should not end with a %s") % os.sep) - else: - for i in range(len(L1s) - 1): - if L1s[i] == SEPARATOR and L1s[i+1] == SEPARATOR: - raise PrefValueKeyComboError(_("Subfolder preferences should not contain two %s one after the other") % os.sep) - return v - - - -class VideoSubfolderPreferences(SubfolderPreferences): - def __init__(self, prefList, parent): - SubfolderPreferences.__init__(self, prefList, parent) - self.prefsDefnL0 = DICT_VIDEO_SUBFOLDER_L0 - self.defaultPrefs = DEFAULT_VIDEO_SUBFOLDER_PREFS - self.defaultRow = [DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0]] - self.L1DateCheck = VIDEO_DATE - self.component = pn.SUBFOLDER_COMPONENT - - def _getMetadataComponent(self): - """ - Returns portion of video / subfolder name based on the metadata - - Note: date time metadata found in _getDateComponent() - """ - return getVideoMetadataComponent(self) - -class Sequences: - """ - Holds sequence numbers and letters used in generating filenames. - The same instance of this class is shared among all threads. - """ - def __init__(self, downloadsToday, storedSequenceNo): - self.subfolderSequenceNo = {} - self.sessionSequenceNo = 1 - self.sequenceLetter = 0 - - self.setUseOfSequenceElements(False, False) - - self.assignedSequenceCounter = 1 - self.reset(downloadsToday, storedSequenceNo) - - def setUseOfSequenceElements(self, usesSessionSequenceNo, usesSequenceLetter): - self.usesSessionSequenceNo = usesSessionSequenceNo - self.usesSequenceLetter = usesSequenceLetter - - def reset(self, downloadsToday, storedSequenceNo): - self.downloadsToday = downloadsToday - self.downloadsTodayOffset = 0 - self.storedSequenceNo = storedSequenceNo - if self.usesSessionSequenceNo: - self.sessionSequenceNo = self.sessionSequenceNo + self.assignedSequenceCounter - 1 - if self.usesSequenceLetter: - self.sequenceLetter = self.sequenceLetter + self.assignedSequenceCounter - 1 - self.doNotAddToPool = False - self.pool = [] - self.poolSequenceCounter = 0 - self.assignedSequenceCounter = 1 - - def getPrelimSequence(self): - if self.doNotAddToPool: - self.doNotAddToPool = False - else: - # increment pool sequence number - self.poolSequenceCounter += 1 - self.pool.append(self.poolSequenceCounter) - - return self.poolSequenceCounter - - def getFinalSequence(self): - # get oldest queue value - # remove from queue or flag it should be removed - - return self.assignedSequenceCounter - - def getSequencePossibilities(self): - for i in self.pool: - yield i - - def getSessionSequenceNo(self): - return self.sessionSequenceNo + self.assignedSequenceCounter - 1 - - def getSessionSequenceNoUsingCounter(self, counter): - return self.sessionSequenceNo + counter - 1 - - def setSessionSequenceNo(self, value): - self.sessionSequenceNo = value - - def setStoredSequenceNo(self, value): - self.storedSequenceNo = value - - def getDownloadsTodayUsingCounter(self, counter): - return self.downloadsToday + counter - self.downloadsTodayOffset - - def setDownloadsToday(self, value): - self.downloadsToday = value - self.downloadsTodayOffset = self.assignedSequenceCounter - 1 - - def getStoredSequenceNoUsingCounter(self, counter): - return self.storedSequenceNo + counter - - def getSequenceLetterUsingCounter(self, counter): - return self.sequenceLetter + counter - 1 - - def imageCopyFailed(self): - self.doNotAddToPool = True - - def imageCopySucceeded(self): - self.increment() - - def increment(self): - assert(self.assignedSequenceCounter == self.pool[0]) - self.assignedSequenceCounter += 1 - self.pool = self.pool[1:] - #assert(len(self.pool) > 0) - - - - -if __name__ == '__main__': - import sys - import os.path - from metadata import MetaData - - if False: - if (len(sys.argv) != 2): - print 'Usage: ' + sys.argv[0] + ' path/to/photo/containing/metadata' - sys.exit(1) - else: - p0 = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE] - p1 = [FILENAME, NAME_EXTENSION, LOWERCASE] - p2 = [METADATA, APERTURE, None] - p3 = [FILENAME, IMAGE_NUMBER, IMAGE_NUMBER_ALL] - p4 = [METADATA, CAMERA_MODEL, ORIGINAL_CASE] - p5 = [TEXT, '-', None] - p6 = [TEXT, 'Job', None] - - p = [p0, p1, p2, p3, p4] - p = [p6 + p5 + p2 + p5 + p3] - - d0 = [DATE_TIME, IMAGE_DATE, 'YYYYMMDD'] - d1 = [DATE_TIME, IMAGE_DATE, 'HHMMSS'] - d2 = [DATE_TIME, IMAGE_DATE, SUBSECONDS] - - d = [d0 + d1 + d2] - - fullpath = sys.argv[1] - path, filename = os.path.split(fullpath) - - m = MetaData(fullpath) - m.readMetadata() - - for pref in p: - i = ImageRenamePreferences(pref, None) - print i.generateNameUsingPreferences(m, filename) - - for pref in d: - i = ImageRenamePreferences(pref, None) - print i.generateNameUsingPreferences(m, filename) - else: - prefs = [SEQUENCES, SESSION_SEQ_NUMBER, SEQUENCE_NUMBER_3] -# prefs = ['Filename2', NAME_EXTENSION, UPPERCASE] - print checkPreferenceValid(DICT_IMAGE_RENAME_L0, prefs) diff --git a/rapid/rpdfile.py b/rapid/rpdfile.py new file mode 100644 index 0000000..03f3af9 --- /dev/null +++ b/rapid/rpdfile.py @@ -0,0 +1,277 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import gtk + +import multiprocessing, logging +logger = multiprocessing.get_logger() + +import pyexiv2 + +import paths + +from gettext import gettext as _ + +import config +import metadataphoto +import metadatavideo + +import problemnotification as pn + +import thumbnail as tn + + +RAW_EXTENSIONS = ['arw', 'dcr', 'cr2', 'crw', 'dng', 'mos', 'mef', 'mrw', + 'nef', 'orf', 'pef', 'raf', 'raw', 'rw2', 'sr2', 'srw'] + +NON_RAW_IMAGE_EXTENSIONS = ['jpg', 'jpe', 'jpeg', 'tif', 'tiff'] + +PHOTO_EXTENSIONS = RAW_EXTENSIONS + NON_RAW_IMAGE_EXTENSIONS + +if metadatavideo.DOWNLOAD_VIDEO: + # some distros do not include the necessary libraries that Rapid Photo Downloader + # needs to be able to download videos + VIDEO_EXTENSIONS = ['3gp', 'avi', 'm2t', 'mov', 'mp4', 'mpeg','mpg', 'mod', + 'tod'] + VIDEO_THUMBNAIL_EXTENSIONS = ['thm'] +else: + VIDEO_EXTENSIONS = [] + VIDEO_THUMBNAIL_EXTENSIONS = [] + + +FILE_TYPE_PHOTO = 0 +FILE_TYPE_VIDEO = 1 + + +def file_type(file_extension): + """ + Uses file extentsion to determine the type of file - photo or video. + + Returns True if yes, else False. + """ + if file_extension in PHOTO_EXTENSIONS: + return FILE_TYPE_PHOTO + elif file_extension in VIDEO_EXTENSIONS: + return FILE_TYPE_VIDEO + return None + +def get_rpdfile(extension, name, display_name, path, size, + file_system_modification_time, + scan_pid, file_id): + + if extension in VIDEO_EXTENSIONS: + return Video(name, display_name, path, size, + file_system_modification_time, + scan_pid, file_id) + else: + # assume it's a photo - no check for performance reasons (this will be + # called many times) + return Photo(name, display_name, path, size, + file_system_modification_time, + scan_pid, file_id) + +class FileTypeCounter: + def __init__(self): + self._counter = dict() + + def add(self, file_type): + self._counter[file_type] = self._counter.setdefault(file_type, 0) + 1 + + def file_types_present(self): + """ + returns a string to be displayed to the user that can be used + to show if a value refers to photos or videos or both, or just one + of each + """ + + no_videos = self._counter.setdefault(FILE_TYPE_VIDEO, 0) + no_images = self._counter.setdefault(FILE_TYPE_PHOTO, 0) + + if (no_videos > 0) and (no_images > 0): + v = _('photos and videos') + elif (no_videos == 0) and (no_images == 0): + v = _('photos or videos') + elif no_videos > 0: + if no_videos > 1: + v = _('videos') + else: + v = _('video') + else: + if no_images > 1: + v = _('photos') + else: + v = _('photo') + return v + + def count_files(self): + i = 0 + for key in self._counter: + i += self._counter[key] + return i + + def summarize_file_count(self): + #Number of files, e.g. "433 photos and videos" or "23 videos". + #Displayed in the progress bar at the top of the main application + #window. + file_types_present = self.file_types_present() + file_count_summary = _("%(number)s %(filetypes)s") % \ + {'number':self.count_files(), + 'filetypes': file_types_present} + return (file_count_summary, file_types_present) + +class RPDFile: + """ + Base class for photo or video file, with metadata + """ + + def __init__(self, name, display_name, path, size, + file_system_modification_time, + scan_pid, file_id): + + self.path = path + + self.name = name + self.display_name = display_name + + self.full_file_name = os.path.join(path, name) + + self.size = size # type int + + self.modification_time = file_system_modification_time + + self.status = config.STATUS_NOT_DOWNLOADED + self.problem = None # class Problem in problemnotifcation.py + + self._assign_file_type() + + self.scan_pid = scan_pid + self.file_id = file_id + self.unique_id = str(scan_pid) + ":" + file_id + + self.problem = None + self.job_code = None + + # generated values + + self.temp_full_file_name = '' + self.download_start_time = None + + self.download_subfolder = '' + self.download_path = '' + self.download_name = '' + self.download_full_file_name = '' + + self.metadata = None + + # Values that will be inserted in download process -- + # (commented out because they're not needed until then) + + #self.sequences = None + #self.download_folder + #self.subfolder_pref_list = [] + #self.name_pref_list = [] + #strip_characters = False + + + def _assign_file_type(self): + self.file_type = None + + def initialize_problem(self): + self.problem = pn.Problem() + + def has_problem(self): + if self.problem is None: + return False + else: + return self.problem.has_problem() + + def add_problem(self, component, problem_definition, *args): + if self.problem is None: + self.initialize_problem() + self.problem.add_problem(component, problem_definition, *args) + + def add_extra_detail(self, extra_detail, *args): + self.problem.add_extra_detail(extra_detail, *args) + + + +#~ exif_tags_needed = ('Exif.Photo.FNumber', + #~ 'Exif.Photo.ISOSpeedRatings', + #~ 'Exif.Photo.ExposureTime', + #~ 'Exif.Photo.FocalLength', + #~ 'Exif.Image.Make', + #~ 'Exif.Image.Model', + #~ 'Exif.Canon.SerialNumber', + #~ 'Exif.Nikon3.SerialNumber' + #~ 'Exif.OlympusEq.SerialNumber', + #~ 'Exif.Olympus.SerialNumber', + #~ 'Exif.Olympus.SerialNumber2', + #~ 'Exif.Panasonic.SerialNumber', + #~ 'Exif.Fujifilm.SerialNumber', + #~ 'Exif.Image.CameraSerialNumber', + #~ 'Exif.Nikon3.ShutterCount', + #~ 'Exif.Canon.FileNumber', + #~ 'Exif.Canon.ImageNumber', + #~ 'Exif.Canon.OwnerName', + #~ 'Exif.Photo.DateTimeOriginal', + #~ 'Exif.Image.DateTime', + #~ 'Exif.Photo.SubSecTimeOriginal', + #~ 'Exif.Image.Orientation' + #~ ) + +class Photo(RPDFile): + + title = _("photo") + title_capitalized = _("Photo") + + def _assign_file_type(self): + self.file_type = FILE_TYPE_PHOTO + + def load_metadata(self): + #~ self.exif_tags = [] + + self.metadata = metadataphoto.MetaData(self.full_file_name) + try: + self.metadata.read() + except: + logger.warning("Could not read metadata from %s" % self.full_file_name) + return False + else: + return True + + + #~ for tag in exif_tags_needed: + #~ if tag in metadata.exif_keys: + #~ self.exif_tags.append(metadata[tag]) + + +class Video(RPDFile): + + title = _("video") + title_capitalized = _("Video") + + def _assign_file_type(self): + self.file_type = FILE_TYPE_VIDEO + + def load_metadata(self): + self.metadata = metadatavideo.VideoMetaData(self.full_file_name) + return True + + diff --git a/rapid/rpdmultiprocessing.py b/rapid/rpdmultiprocessing.py new file mode 100644 index 0000000..7fdd252 --- /dev/null +++ b/rapid/rpdmultiprocessing.py @@ -0,0 +1,25 @@ +# -*- coding: latin1 -*- +### Copyright (C) 2007, 2008, 2009, 2010, 2011 Damon Lynch + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +CONN_PARTIAL = 0 +CONN_COMPLETE = 1 + +MSG_BYTES = 0 +MSG_FILE = 1 +MSG_TEMP_DIRS = 2 + +MSG_SEQUENCE_VALUE = 0 diff --git a/rapid/scan.py b/rapid/scan.py new file mode 100755 index 0000000..ce882b4 --- /dev/null +++ b/rapid/scan.py @@ -0,0 +1,170 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import multiprocessing + +import gio +import gtk + +import pyexiv2 + +import rpdmultiprocessing as rpdmp +import rpdfile + + +import logging +logger = multiprocessing.get_logger() + +# python whitespace is significant - don't remove the leading whitespace on +# the second line + +file_attributes = "standard::name,standard::display-name,\ +standard::type,standard::size,time::modified,access::can-read,id::file" + + + +class Scan(multiprocessing.Process): + + """Scans the given path for files of a specified type. + + Returns results in batches, finishing with a total of the size of all the + files in bytes. + """ + + def __init__(self, path, batch_size, generate_folder, results_pipe, + terminate_queue, run_event): + + """Setup values needed to conduct the scan. + + 'path' is a string of the path to be scanned, which is passed to gio. + + 'batch_size' is the number of files that should be sent back to the + calling function at one time. + + 'results_pipe' is a connection on which to send the results. + + 'terminate_queue' is a queue whose sole purpose is to notify the + process that it should terminate and not return any results. + + 'run_event' is an Event that is used to temporarily halt execution. + + """ + + multiprocessing.Process.__init__(self) + self.path = path + self.results_pipe = results_pipe + self.terminate_queue = terminate_queue + self.run_event = run_event + self.batch_size = batch_size + self.generate_folder = generate_folder + self.counter = 0 + self.files = [] + self.file_type_counter = rpdfile.FileTypeCounter() + + + def _gio_scan(self, path, file_size_sum): + """recursive function to scan a directory and its subdirectories + for photos and possibly videos""" + + children = path.enumerate_children(file_attributes) + + for child in children: + + # pause if instructed by the caller + self.run_event.wait() + + if not self.terminate_queue.empty(): + x = self.terminate_queue.get() + # terminate immediately + print "terminating..." + self.files = [] + return None + + # only collect files and scan in directories we can actually read + # cannot assume that users will download only from memory cards + + if child.get_attribute_boolean(gio.FILE_ATTRIBUTE_ACCESS_CAN_READ): + file_type = child.get_file_type() + name = child.get_name() + if file_type == gio.FILE_TYPE_DIRECTORY: + file_size_sum = self._gio_scan(path.get_child(name), + file_size_sum) + if file_size_sum is None: + return None + + elif file_type == gio.FILE_TYPE_REGULAR: + ext = os.path.splitext(name)[1].lower()[1:] + + file_type = rpdfile.file_type(ext) + if file_type is not None: + # count how many files of each type are included + # e.g. photo, video + self.file_type_counter.add(file_type) + self.counter += 1 + display_name = child.get_display_name() + size = child.get_size() + modification_time = child.get_modification_time() + file_id = child.get_attribute_string( + gio.FILE_ATTRIBUTE_ID_FILE) + scanned_file = rpdfile.get_rpdfile(ext, + name, + display_name, + path.get_path(), + size, + modification_time, + self.pid, + file_id) + + if self.generate_folder: + # this dramatically slows scanning speed, and it + # is unlikely this will be called this early in the + # workflow + scanned_file.read_metadata() + + self.files.append(scanned_file) + + if self.counter == self.batch_size: + # send batch of results + self.results_pipe.send((rpdmp.CONN_PARTIAL, + self.files)) + self.files = [] + self.counter = 0 + + file_size_sum += size + + return file_size_sum + + + def run(self): + """start the actual scan.""" + source = gio.File(self.path) + try: + size = self._gio_scan(source, 0) + except gio.Error, inst: + logger.error("Error while scanning %s: %s", self.path, inst) + size = None + + if size is not None: + if self.counter > 0: + # send any remaining results + self.results_pipe.send((rpdmp.CONN_PARTIAL, self.files)) + self.results_pipe.send((rpdmp.CONN_COMPLETE, (size, + self.file_type_counter, self.pid))) + self.results_pipe.close() diff --git a/rapid/subfolderfile.py b/rapid/subfolderfile.py new file mode 100644 index 0000000..53118c2 --- /dev/null +++ b/rapid/subfolderfile.py @@ -0,0 +1,259 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +Generates names for files and folders. + +Runs a daemon process. +""" + +import os + +import gio +import multiprocessing +import logging +logger = multiprocessing.get_logger() + + +import rpdfile +import rpdmultiprocessing as rpdmp +import generatename as gn +import problemnotification as pn +import prefsrapid +import config + +from gettext import gettext as _ + + + +def _generate_name(generator, rpd_file): + + do_generation = True + if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: + if rpd_file.metadata is None: + if not rpd_file.load_metadata(): + # Error in reading metadata + rpd_file.add_problem(None, pn.CANNOT_DOWNLOAD_BAD_METADATA, {'filetype': rpd_file.title_capitalized}) + do_generation = False + else: + if rpd_file.metadata is None: + rpd_file.load_metadata() + + if do_generation: + value = generator.generate_name(rpd_file) + if value is None: + 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] + self.refresh_downloads_today = sequence_values[3] + self.stored_sequence_no = sequence_values[4] + self.uses_stored_sequence_no = sequence_values[5] + self.uses_session_sequece_no = sequence_values[6] + self.uses_sequence_letter = sequence_values[7] + + logger.debug("Start of day is set to %s", self.day_start.value) + + + + + def progress_callback_no_update(self, amount_downloaded, total): + pass + + def run(self): + """ + Get subfolder and name. + Attempt to move the file from it's temporary directory. + If successful, increment sequence values. + Report any success or failure. + """ + i = 0 + download_count = 0 + + + # 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.stored_sequence_no.value) + + + while True: + logger.debug("Finished %s. Getting next task.", download_count) + + task = self.results_pipe.recv() + + # rename file and move to generated subfolder + download_succeeded, download_count, rpd_file = task + + move_succeeded = False + + + if download_succeeded: + temp_file = gio.File(rpd_file.temp_full_file_name) + + # Generate subfolder name and new file name + generation_succeeded = True + rpd_file = generate_subfolder(rpd_file) + if rpd_file.download_subfolder: + + if self.refresh_downloads_today.value: + # overwrite downloads today value tracked here, + # as user has modified their preferences + self.downloads_today_tracker.set_raw_downloads_today_from_int(self.downloads_today.value) + 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(): + rpd_file.status = config.STATUS_DOWNLOADED_WITH_WARNING + + # 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: + area = _("subfolder and filename") + elif not rpd_file.download_name: + area = _("filename") + else: + area = _("subfolder") + rpd_file.add_problem(None, pn.ERROR_IN_NAME_GENERATION, {'filetype': rpd_file.title_capitalized, 'area': area}) + rpd_file.add_extra_detail(pn.NO_DATA_TO_NAME, {'filetype': area}) + generation_succeeded = False + rpd_file.status = config.STATUS_DOWNLOAD_FAILED + # FIXME: log error + + 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) + + 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 + + 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 directory: %s", rpd_file.download_path) + logger.error(inst) + + # Move temp file to subfolder + + 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 + if rpd_file.status <> config.STATUS_DOWNLOADED_WITH_WARNING: + rpd_file.status = config.STATUS_DOWNLOADED + except gio.Error, inst: + rpd_file.add_problem(None, pn.DOWNLOAD_COPYING_ERROR, {'filetype': rpd_file.title}) + 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) + + logger.debug("Finish processing file: %s", download_count) + + if move_succeeded: + if self.uses_session_sequece_no.value or self.uses_sequence_letter.value: + self.sequences.increment( + 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 not move_succeeded: + 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) + + + + + + rpd_file.metadata = None #purge metadata, as it cannot be pickled + self.results_pipe.send((move_succeeded, rpd_file,)) + + i += 1 + + + diff --git a/rapid/tableplusminus.py b/rapid/tableplusminus.py index ab7e7c1..5713474 100644 --- a/rapid/tableplusminus.py +++ b/rapid/tableplusminus.py @@ -44,15 +44,15 @@ class TablePlusMinus(gtk.Table): def __init__(self, rows=1, columns=1, homogeneous=False): if not self.debug: gtk.Table.__init__(self, rows, columns + 2, homogeneous) - self.extraCols = 2 # representing minus and plus buttons + self.extra_cols = 2 # representing minus and plus buttons else: gtk.Table.__init__(self, rows, columns + 3, homogeneous) - self.extraCols = 3 # representing minus and plus buttons, and info label + self.extra_cols = 3 # representing minus and plus buttons, and info label # no of columns NOT including the + and - buttons - self.pm_noColumns = columns + self.pm_no_columns = columns # how many rows there are in the gtk.Table - self.pm_noRows = rows + self.pm_no_rows = rows # list of widgets in the gtk.Table self.pm_rows = [] # dict of callback ids for minus and plus buttons @@ -67,39 +67,39 @@ class TablePlusMinus(gtk.Table): self.set_col_spacing(columns+1, hd.CONTROL_IN_TABLE_SPACE) self.set_row_spacings(hd.CONTROL_IN_TABLE_SPACE) - def _setMinusButtonSensitivity(self): - button = self.pm_rows[0][self.pm_noColumns] + def _set_minus_button_sensitivity(self): + button = self.pm_rows[0][self.pm_no_columns] if len(self.pm_rows) == 1: button.set_sensitive(False) else: button.set_sensitive(True) - def _createMinusPlusButtons(self, rowPosition): + def _create_minus_plus_buttons(self, row_position): plus_button = gtk.Button() plus_button.set_image(gtk.image_new_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_MENU)) - self._createCallback(plus_button, rowPosition, 'clicked', self.on_plus_button_clicked) + self._create_callback(plus_button, row_position, 'clicked', self.on_plus_button_clicked) minus_button = gtk.Button() minus_button.set_image(gtk.image_new_from_stock(gtk.STOCK_REMOVE, gtk.ICON_SIZE_MENU)) - self._createCallback(minus_button, rowPosition, 'clicked', self.on_minus_button_clicked) + self._create_callback(minus_button, row_position, 'clicked', self.on_minus_button_clicked) return minus_button, plus_button def append(self, row): - self.insertAfter(len(self.pm_rows)-1, row) + self.insert_after(len(self.pm_rows)-1, row) - def _getMinusAndPlusButtonsForRow(self, rowPosition): + def _get_minus_and_plus_buttons_for_row(self, row_position): """ - Return as a tuple minus and plus buttons for the row specified by rowPosition + Return as a tuple minus and plus buttons for the row specified by row_position """ - return (self.pm_rows[rowPosition][self.pm_noColumns], self.pm_rows[rowPosition][self.pm_noColumns+1]) + return (self.pm_rows[row_position][self.pm_no_columns], self.pm_rows[row_position][self.pm_no_columns+1]) - def removeRow(self, rowPosition): + def remove_row(self, row_position): # remove widgets from table - for col in range(self.pm_noColumns + self.extraCols): - widget = self.pm_rows[rowPosition][col] + for col in range(self.pm_no_columns + self.extra_cols): + widget = self.pm_rows[row_position][col] if widget: self.remove(widget) if self.pm_callbacks.has_key(widget): @@ -108,61 +108,61 @@ class TablePlusMinus(gtk.Table): # reposition existing rows in gtk.Table - self._moveRows(-1, rowPosition + 1) + self._move_rows(-1, row_position + 1) # remove row from list of rows - del self.pm_rows[rowPosition] + del self.pm_rows[row_position] - self._setMinusButtonSensitivity() - self.pm_noRows -= 1 - self.resize(self.pm_noRows, self.pm_noColumns + self.extraCols) - self._printDebugInfo() + self._set_minus_button_sensitivity() + self.pm_no_rows -= 1 + self.resize(self.pm_no_rows, self.pm_no_columns + self.extra_cols) + self._print_debug_info() - def _createCallback(self, widget, rowPosition, callbackType = None, callbackMethod=None): - if callbackType: - self.pm_callbacks[widget] = widget.connect(callbackType, callbackMethod, rowPosition) + def _create_callback(self, widget, row_position, callback_type = None, callbackMethod=None): + if callback_type: + self.pm_callbacks[widget] = widget.connect(callback_type, callbackMethod, row_position) else: name = widget.get_name() if name == 'GtkComboBox': - self.pm_callbacks[widget] = widget.connect("changed", self.on_combobox_changed, rowPosition) + self.pm_callbacks[widget] = widget.connect("changed", self.on_combobox_changed, row_position) elif name == 'GtkEntry': - self.pm_callbacks[widget] = widget.connect("changed", self.on_entry_changed, rowPosition) + self.pm_callbacks[widget] = widget.connect("changed", self.on_entry_changed, row_position) - def _moveRows(self, adjustment, startRow, endRow = -1): + def _move_rows(self, adjustment, start_row, end_row = -1): """ Moves gtk.Table rows up or down according to adjustment (which MUST be -1 or 1). - Starts at row startRow and ends at row endRow. If endRow == -1, then goes to last row in table. + Starts at row start_row and ends at row end_row. If end_row == -1, then goes to last row in table. Readjusts callbacks. """ - if endRow == -1: - endRow = len(self.pm_rows) - for r in range(startRow, endRow): + if end_row == -1: + end_row = len(self.pm_rows) + for r in range(start_row, end_row): if self.debug: - print "Row %s becomes row %s" % (self.pm_rows[r][self.pm_noColumns + 2].get_label(), r + adjustment) - self.pm_rows[r][self.pm_noColumns + 2].set_label(str(r + adjustment)) + print "Row %s becomes row %s" % (self.pm_rows[r][self.pm_no_columns + 2].get_label(), r + adjustment) + self.pm_rows[r][self.pm_no_columns + 2].set_label(str(r + adjustment)) - for col in range(self.pm_noColumns + self.extraCols): + for col in range(self.pm_no_columns + self.extra_cols): widget = self.pm_rows[r][col] if widget: self.remove(widget) widget.disconnect(self.pm_callbacks[widget]) self.attach(widget, col, col+1, r + adjustment, r + adjustment + 1) - if col == self.pm_noColumns: - self._createCallback(widget, r + adjustment, 'clicked', self.on_minus_button_clicked) - elif col == self.pm_noColumns + 1: - self._createCallback(widget, r + adjustment, 'clicked', self.on_plus_button_clicked) + if col == self.pm_no_columns: + self._create_callback(widget, r + adjustment, 'clicked', self.on_minus_button_clicked) + elif col == self.pm_no_columns + 1: + self._create_callback(widget, r + adjustment, 'clicked', self.on_plus_button_clicked) else: - self._createCallback(widget, r + adjustment) + self._create_callback(widget, r + adjustment) - def _printDebugInfo(self): + def _print_debug_info(self): if self.debug: print "\nRows in internal list: %s\nTable rows: %s" % \ - (len(self.pm_rows), self.pm_noRows) + (len(self.pm_rows), self.pm_no_rows) - if len(self.pm_rows) <> self.pm_noRows: + if len(self.pm_rows) <> self.pm_no_rows: print "|\n\\\n --> Unequal no. of rows" @@ -172,42 +172,42 @@ class TablePlusMinus(gtk.Table): """ Override base class attach method, to allow automatic shrinking of minus and plus buttons """ - if left_attach >= self.pm_noColumns and left_attach <= self.pm_noColumns + 1: + if left_attach >= self.pm_no_columns and left_attach <= self.pm_no_columns + 1: # since we are adding plus or minus button, shrink the button gtk.Table.attach(self, child, left_attach, right_attach, top_attach, bottom_attach, gtk.SHRINK, gtk.SHRINK, xpadding, ypadding) else: gtk.Table.attach(self, child, left_attach, right_attach, top_attach, bottom_attach, xoptions, yoptions, xpadding, ypadding) - def insertAfter(self, rowPosition, row): + def insert_after(self, row_position, row): """ - Inserts row into the table at row following rowPosition + Inserts row into the table at row following row_position """ #is table big enough? - self.checkTableRowsAndAdjust() + self.check_table_rows_and_adjust() #move (reattach) other widgets & readjust connect - self._moveRows(1, rowPosition + 1) + self._move_rows(1, row_position + 1) # insert row - for col in range(self.pm_noColumns): + for col in range(self.pm_no_columns): widget = row[col] if widget: - self._createCallback(widget, rowPosition+1) - self.attach(widget, col, col+1, rowPosition+1, rowPosition+2) + self._create_callback(widget, row_position+1) + self.attach(widget, col, col+1, row_position+1, row_position+2) - minus_button, plus_button = self._createMinusPlusButtons(rowPosition+1) + minus_button, plus_button = self._create_minus_plus_buttons(row_position+1) row.append(minus_button) row.append(plus_button) - self.attach(minus_button, self.pm_noColumns, self.pm_noColumns+1, rowPosition+1, rowPosition+2) - self.attach(plus_button, self.pm_noColumns+1, self.pm_noColumns+2, rowPosition+1, rowPosition+2) + self.attach(minus_button, self.pm_no_columns, self.pm_no_columns+1, row_position+1, row_position+2) + self.attach(plus_button, self.pm_no_columns+1, self.pm_no_columns+2, row_position+1, row_position+2) if self.debug: - label = gtk.Label(str(rowPosition+1)) - self.attach(label, self.pm_noColumns+2, self.pm_noColumns+3, rowPosition+1, rowPosition+2) + label = gtk.Label(str(row_position+1)) + self.attach(label, self.pm_no_columns+2, self.pm_no_columns+3, row_position+1, row_position+2) row.append(label) @@ -217,66 +217,66 @@ class TablePlusMinus(gtk.Table): #adjust internal reference table - self.pm_rows.insert(rowPosition + 1, row) + self.pm_rows.insert(row_position + 1, row) - self._setMinusButtonSensitivity() + self._set_minus_button_sensitivity() - self._printDebugInfo() + self._print_debug_info() - def checkTableRowsAndAdjust(self, noRowsToAdd=1, adjustRows=True): - noRowsOk = True - if len(self.pm_rows) + noRowsToAdd > self.pm_noRows: - if adjustRows: - extraRowsToAdd = len(self.pm_rows) + noRowsToAdd - self.pm_noRows - self.pm_noRows += extraRowsToAdd - self.resize(self.pm_noRows, self.pm_noColumns + self.extraCols) + def check_table_rows_and_adjust(self, no_rows_to_add=1, adjust_rows=True): + no_rows_ok = True + if len(self.pm_rows) + no_rows_to_add > self.pm_no_rows: + if adjust_rows: + extra_rows_to_add = len(self.pm_rows) + no_rows_to_add - self.pm_no_rows + self.pm_no_rows += extra_rows_to_add + self.resize(self.pm_no_rows, self.pm_no_columns + self.extra_cols) else: - noRowsOk = False - return noRowsOk + no_rows_ok = False + return no_rows_ok - def getDefaultRow(self): + def get_default_row(self): """ Returns a list of default widgets to insert as a row into the table. Expected to be implemented in derived class. """ - return [None] * self.pm_noColumns + return [None] * self.pm_no_columns - def on_combobox_changed(self, widget, rowPosition): + def on_combobox_changed(self, widget, row_position): """ Callback for combobox that is expected to be implemented in derived class """ pass - def on_entry_changed(self, widget, rowPosition): + def on_entry_changed(self, widget, row_position): """ Callback for entry that is expected to be implemented in derived class """ pass - def _debugButtonPressed(self, buttonText, rowPosition): + def _debug_button_pressed(self, buttonText, row_position): if self.debug: t = datetime.datetime.now().strftime("%H:%M:%S") - print "\n****\n%s\n\n%s clicked at %s" %(t, buttonText, rowPosition) + print "\n****\n%s\n\n%s clicked at %s" %(t, buttonText, row_position) - def on_minus_button_clicked(self, widget, rowPosition): - self._debugButtonPressed("Minus", rowPosition) - self.removeRow(rowPosition) - self.on_rowDeleted(rowPosition) + def on_minus_button_clicked(self, widget, row_position): + self._debug_button_pressed("Minus", row_position) + self.remove_row(row_position) + self.on_row_deleted(row_position) - def on_plus_button_clicked(self, widget, rowPosition): - self._debugButtonPressed("Plus", rowPosition) - self.insertAfter(rowPosition, self.getDefaultRow()) - self.on_rowAdded(rowPosition) + def on_plus_button_clicked(self, widget, row_position): + self._debug_button_pressed("Plus", row_position) + self.insert_after(row_position, self.get_default_row()) + self.on_row_added(row_position) - def on_rowAdded(self, rowPosition): + def on_row_added(self, row_position): """ Expected to be implemented in derived class """ pass - def on_rowDeleted(self, rowPosition): + def on_row_deleted(self, row_position): """ Expected to be implemented in derived class """ diff --git a/rapid/thumbnail.py b/rapid/thumbnail.py new file mode 100644 index 0000000..9df2147 --- /dev/null +++ b/rapid/thumbnail.py @@ -0,0 +1,393 @@ +#!/usr/bin/python +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import multiprocessing +import types +import os + +import gtk + +import paths + +from PIL import Image +import cStringIO +import tempfile +import subprocess + +import rpdfile + +import rpdmultiprocessing as rpdmp +from utilities import image_to_pixbuf, pixbuf_to_image +import pyexiv2 + +from filmstrip import add_filmstrip + +import logging +logger = multiprocessing.get_logger() + + +def get_stock_photo_image(): + length = min(gtk.gdk.screen_width(), gtk.gdk.screen_height()) + pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(paths.share_dir('glade3/photo.svg'), length, length) + image = pixbuf_to_image(pixbuf) + return image + +def get_stock_photo_image_icon(): + image = Image.open(paths.share_dir('glade3/photo66.png')) + image = image.convert("RGBA") + return image + +def get_stock_video_image(): + length = min(gtk.gdk.screen_width(), gtk.gdk.screen_height()) + pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(paths.share_dir('glade3/video.svg'), length, length) + image = pixbuf_to_image(pixbuf) + return image + +def get_stock_video_image_icon(): + image = Image.open(paths.share_dir('glade3/video66.png')) + image = image.convert("RGBA") + return image + + +class PhotoIcons(): + stock_thumbnail_image_icon = get_stock_photo_image_icon() + +class VideoIcons(): + stock_thumbnail_image_icon = get_stock_video_image_icon() + +def upsize_pil(image, size): + width_max = size[0] + height_max = size[1] + width_orig = float(image.size[0]) + height_orig = float(image.size[1]) + if (width_orig / width_max) > (height_orig / height_max): + height = int((height_orig / width_orig) * width_max) + width = width_max + else: + width = int((width_orig / height_orig) * height_max) + height=height_max + + return image.resize((width, height), Image.ANTIALIAS) + +def downsize_pil(image, box, fit=False): + """Downsample the PIL image. + image: Image - an Image-object + box: tuple(x, y) - the bounding box of the result image + fix: boolean - crop the image to fill the box + + Code adpated from example by Christian Harms + Source: http://united-coders.com/christian-harms/image-resizing-tips-every-coder-should-know + """ + #preresize image with factor 2, 4, 8 and fast algorithm + factor = 1 + logger.debug("Image size %sx%s", image.size[0], image.size[1]) + logger.debug("Box size %sx%s", box[0],box[1]) + while image.size[0]/factor > 2*box[0] and image.size[1]*2/factor > 2*box[1]: + factor *=2 + if factor > 1: + logger.debug("quick resize %sx%s", image.size[0]/factor, image.size[1]/factor) + image.thumbnail((image.size[0]/factor, image.size[1]/factor), Image.NEAREST) + logger.debug("did first thumbnail") + + #calculate the cropping box and get the cropped part + if fit: + x1 = y1 = 0 + x2, y2 = image.size + wRatio = 1.0 * x2/box[0] + hRatio = 1.0 * y2/box[1] + if hRatio > wRatio: + y1 = y2/2-box[1]*wRatio/2 + y2 = y2/2+box[1]*wRatio/2 + else: + x1 = x2/2-box[0]*hRatio/2 + x2 = x2/2+box[0]*hRatio/2 + image = image.crop((x1,y1,x2,y2)) + + #Resize the image with best quality algorithm ANTI-ALIAS + logger.debug("about to actually downsize using image.thumbnail") + image.thumbnail(box, Image.ANTIALIAS) + logger.debug("it downsized") + +class PicklablePIL: + def __init__(self, image): + self.size = image.size + self.mode = image.mode + self.image_data = image.tostring() + + def get_image(self): + return Image.fromstring(self.mode, self.size, self.image_data) + + def get_pixbuf(self): + return image_to_pixbuf(self.get_image()) + +def get_video_THM_file(fullFileName): + """ + Checks to see if a thumbnail file (THM) is in the same directory as the + file. Expects a full path to be part of the file name. + + Returns the filename, including path, if found, else returns None. + """ + + f = None + name, ext = os.path.splitext(fullFileName) + for e in rpdfile.VIDEO_THUMBNAIL_EXTENSIONS: + if os.path.exists(name + '.' + e): + f = name + '.' + e + break + if os.path.exists(name + '.' + e.upper()): + f = name + '.' + e.upper() + break + + return f + +class Thumbnail: + + # file types from which to remove letterboxing (black bands in the thumbnail + # previews) + crop_thumbnails = ('CR2', 'DNG', 'RAF', 'ORF', 'PEF', 'ARW') + + def _ignore_embedded_160x120_thumbnail(self, max_size_needed, metadata): + return max_size_needed is None or max_size_needed[0] > 160 or max_size_needed[1] > 120 or not metadata.exif_thumbnail.data + + def _get_thumbnail_data(self, metadata, max_size_needed): + logger.debug("Getting thumbnail data %s", max_size_needed) + if self._ignore_embedded_160x120_thumbnail(max_size_needed, metadata): + logger.debug("Ignoring embedded preview") + lowrez = False + previews = metadata.previews + if not previews: + return (None, None) + else: + if max_size_needed: + for thumbnail in previews: + if thumbnail.dimensions[0] >= max_size_needed or thumbnail.dimensions[1] >= max_size_needed: + break + else: + thumbnail = previews[-1] + else: + thumbnail = metadata.exif_thumbnail + lowrez = True + return (thumbnail.data, lowrez) + + def _process_thumbnail(self, image, size_reduced): + if image.mode <> "RGBA": + image = image.convert("RGBA") + + thumbnail = PicklablePIL(image) + if size_reduced is not None: + thumbnail_icon = image.copy() + downsize_pil(thumbnail_icon, size_reduced, fit=False) + thumbnail_icon = PicklablePIL(thumbnail_icon) + else: + thumbnail_icon = None + + return (thumbnail, thumbnail_icon) + + def _get_photo_thumbnail(self, full_file_name, size_max, size_reduced): + thumbnail = None + thumbnail_icon = None + name = os.path.basename(full_file_name) + metadata = pyexiv2.metadata.ImageMetadata(full_file_name) + try: + logger.debug("Read photo metadata...") + metadata.read() + except: + logger.warning("Could not read metadata from %s", full_file_name) + else: + logger.debug("...successfully read photo metadata") + if metadata.mime_type == "image/jpeg" and self._ignore_embedded_160x120_thumbnail(size_max, metadata): + try: + image = Image.open(full_file_name) + lowrez = False + except: + logger.warning("Could not generate thumbnail for jpeg %s ", full_file_name) + image = None + else: + thumbnail_data, lowrez = self._get_thumbnail_data(metadata, max_size_needed=size_max) + logger.debug("_get_thumbnail_data returned") + if not isinstance(thumbnail_data, types.StringType): + image = None + else: + td = cStringIO.StringIO(thumbnail_data) + logger.debug("got td") + try: + image = Image.open(td) + except: + logger.warning("Unreadable thumbnail for %s", full_file_name) + image = None + logger.debug("opened image") + if image: + try: + orientation = metadata['Exif.Image.Orientation'].value + except: + orientation = None + if lowrez: + # need to remove letterboxing / pillarboxing from some + # RAW thumbnails + if os.path.splitext(full_file_name)[1][1:].upper() in Thumbnail.crop_thumbnails: + image2 = image.crop((0, 8, 160, 112)) + image2.load() + image = image2 + if size_max is not None and (image.size[0] > size_max[0] or image.size[1] > size_max[1]): + logger.debug("downsizing") + downsize_pil(image, size_max, fit=False) + logger.debug("downsized") + if orientation == 8: + # rotate counter clockwise + image = image.rotate(90) + elif orientation == 6: + # rotate clockwise + image = image.rotate(270) + elif orientation == 3: + # rotate upside down + image = image.rotate(180) + thumbnail, thumbnail_icon = self._process_thumbnail(image, size_reduced) + + logger.debug("...got thumbnail for %s", full_file_name) + return (thumbnail, thumbnail_icon) + + def _get_video_thumbnail(self, full_file_name, size_max, size_reduced): + thumbnail = None + thumbnail_icon = None + if size_max is None: + size = 0 + else: + size = max(size_max[0], size_max[1]) + image = None + if size > 0 and size <= 160: + thm = get_video_THM_file(full_file_name) + if thm: + try: + thumbnail = gtk.gdk.pixbuf_new_from_file(thm) + except: + logger.warning("Could not open THM file for %s", full_file_name) + thumbnail = add_filmstrip(thumbnail) + image = pixbuf_to_image(thumbnail) + + if image is None: + try: + tmp_dir = tempfile.mkdtemp(prefix="rpd-tmp") + thm = os.path.join(tmp_dir, 'thumbnail.jpg') + subprocess.check_call(['ffmpegthumbnailer', '-i', full_file_name, '-t', '10', '-f', '-o', thm, '-s', str(size)]) + image = Image.open(thm) + image.load() + os.unlink(thm) + os.rmdir(tmp_dir) + except: + image = None + logger.error("Error generating thumbnail for %s", full_file_name) + if image: + thumbnail, thumbnail_icon = self._process_thumbnail(image, size_reduced) + + logger.debug("...got thumbnail for %s", full_file_name) + return (thumbnail, thumbnail_icon) + + def get_thumbnail(self, full_file_name, file_type, size_max=None, size_reduced=None): + logger.debug("Getting thumbnail for %s...", full_file_name) + if file_type == rpdfile.FILE_TYPE_PHOTO: + logger.debug("file type is photo") + return self._get_photo_thumbnail(full_file_name, size_max, size_reduced) + else: + return self._get_video_thumbnail(full_file_name, size_max, size_reduced) + + +class GetPreviewImage(multiprocessing.Process): + def __init__(self, results_pipe): + multiprocessing.Process.__init__(self) + self.daemon = True + self.results_pipe = results_pipe + self.thumbnail_maker = Thumbnail() + self.stock_photo_thumbnail_image = None + self.stock_video_thumbnail_image = None + + def get_stock_image(self, file_type): + """ + Get stock image for file type scaled to the current size of the + """ + if file_type == rpdfile.FILE_TYPE_PHOTO: + if self.stock_photo_thumbnail_image is None: + self.stock_photo_thumbnail_image = PicklablePIL(get_stock_photo_image()) + return self.stock_photo_thumbnail_image + else: + if self.stock_video_thumbnail_image is None: + self.stock_video_thumbnail_image = PicklablePIL(get_stock_video_image()) + return self.stock_video_thumbnail_image + + def run(self): + while True: + unique_id, full_file_name, file_type, size_max = self.results_pipe.recv() + full_size_preview, reduced_size_preview = self.thumbnail_maker.get_thumbnail(full_file_name, file_type, size_max=size_max, size_reduced=None) + if full_size_preview is None: + full_size_preview = self.get_stock_image(file_type) + self.results_pipe.send((unique_id, full_size_preview, reduced_size_preview)) + + + +class GenerateThumbnails(multiprocessing.Process): + def __init__(self, files, batch_size, results_pipe, terminate_queue, + run_event): + multiprocessing.Process.__init__(self) + self.results_pipe = results_pipe + self.terminate_queue = terminate_queue + self.batch_size = batch_size + self.files = files + self.run_event = run_event + self.results = [] + + self.thumbnail_maker = Thumbnail() + + + def run(self): + counter = 0 + i = 0 + for f in self.files: + + # pause if instructed by the caller + self.run_event.wait() + + if not self.terminate_queue.empty(): + x = self.terminate_queue.get() + # terminate immediately + logger.info("Terminating thumbnailing") + return None + + + thumbnail, thumbnail_icon = self.thumbnail_maker.get_thumbnail( + f.full_file_name, + f.file_type, + (160, 120), (100,100)) + + #~ logger.debug("Appending results for %s" %f.full_file_name) + self.results.append((f.unique_id, thumbnail_icon, thumbnail)) + counter += 1 + if counter == self.batch_size: + #~ logger.debug("Sending results....") + self.results_pipe.send((rpdmp.CONN_PARTIAL, self.results)) + self.results = [] + counter = 0 + i += 1 + + if counter > 0: + # send any remaining results + #~ logger.debug("Sending final results....") + self.results_pipe.send((rpdmp.CONN_PARTIAL, self.results)) + self.results_pipe.send((rpdmp.CONN_COMPLETE, None)) + self.results_pipe.close() + diff --git a/rapid/utilities.py b/rapid/utilities.py new file mode 100644 index 0000000..07be833 --- /dev/null +++ b/rapid/utilities.py @@ -0,0 +1,150 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007 - 2011 Damon Lynch + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import gio +import gtk +from PIL import Image +import distutils.version + +def get_full_path(path): + """ make path relative to home directory if not an absolute path """ + if os.path.isabs(path): + return path + else: + return os.path.join(os.path.expanduser('~'), path) + +def is_directory(path): + + # for some very strange reason, doing it the GIO way fails with + # unknown type, even for directories! + return os.path.isdir(path) + + if False: + d = gio.File(path) + if d.query_exists(): + file_info = d.query_filesystem_info(attributes="standard::type") + file_type = file_info.get_file_type() + if file_type == gio.FILE_TYPE_DIRECTORY: + return True + + return False + +def format_size_for_user(bytes, zero_string="", with_decimals=True, kb_only=False): + """Format an int containing the number of bytes into a string suitable for + printing out to the user. zero_string is the string to use if bytes == 0. + source: https://develop.participatoryculture.org/trac/democracy/browser/trunk/tv/portable/util.py?rev=3993 + + """ + if bytes > (1 << 30) and not kb_only: + value = (bytes / (1024.0 * 1024.0 * 1024.0)) + if with_decimals: + format = "%1.1fGB" + else: + format = "%dGB" + elif bytes > (1 << 20) and not kb_only: + value = (bytes / (1024.0 * 1024.0)) + if with_decimals: + format = "%1.1fMB" + else: + format = "%dMB" + elif bytes > (1 << 10): + value = (bytes / 1024.0) + if with_decimals: + format = "%1.1fKB" + else: + format = "%dKB" + elif bytes > 1: + value = bytes + if with_decimals: + format = "%1.1fB" + else: + format = "%dB" + else: + return zero_string + return format % value + +def register_iconsets(icon_info): + """ + Register icons in the icon set if they're not already used + + From http://faq.pygtk.org/index.py?req=show&file=faq08.012.htp + """ + + icon_factory = gtk.IconFactory() + stock_ids = gtk.stock_list_ids() + for stock_id, file in icon_info: + # only load image files when our stock_id is not present + if stock_id not in stock_ids: + pixbuf = gtk.gdk.pixbuf_new_from_file(file) + iconset = gtk.IconSet(pixbuf) + icon_factory.add(stock_id, iconset) + icon_factory.add_default() + +def escape(s): + """ + Replace special characters by SGML entities. + """ + entities = ("&&", "<<", ">>") + for e in entities: + s = s.replace(e[0], e[1:]) + return s + +def image_to_pixbuf(image): + # convert PIL image to pixbuf + # this one handles transparency, unlike the default example in the pygtk FAQ + # this is also from the pygtk FAQ + IS_RGBA = image.mode=='RGBA' + return gtk.gdk.pixbuf_new_from_data( + image.tostring(), # data + gtk.gdk.COLORSPACE_RGB, # color mode + IS_RGBA, # has alpha + 8, # bits + image.size[0], # width + image.size[1], # height + (IS_RGBA and 4 or 3) * image.size[0] # rowstride + ) + +def pixbuf_to_image(pb): + assert(pb.get_colorspace() == gtk.gdk.COLORSPACE_RGB) + dimensions = pb.get_width(), pb.get_height() + stride = pb.get_rowstride() + pixels = pb.get_pixels() + + mode = pb.get_has_alpha() and "RGBA" or "RGB" + image = Image.frombuffer(mode, dimensions, pixels, + "raw", mode, stride, 1) + + if mode == "RGB": + # convert to having an alpha value, so that the image can + # act as a mask in the drop shadow paste + image = image.convert("RGBA") + + return image + +def pythonify_version(v): + """ makes version number a version number in distutils sense""" + return distutils.version.StrictVersion(v.replace( '~','')) + +def human_readable_version(v): + """ returns a version in human readable form""" + v = v.replace('~a', ' alpha ') + v = v.replace('~b', ' beta ') + return v + diff --git a/rapid/videometadata.py b/rapid/videometadata.py deleted file mode 100755 index 77f6791..0000000 --- a/rapid/videometadata.py +++ /dev/null @@ -1,250 +0,0 @@ -#!/usr/bin/python -# -*- coding: latin1 -*- - -### Copyright (C) 2007-10 Damon Lynch - -### This program is free software; you can redistribute it and/or modify -### it under the terms of the GNU General Public License as published by -### the Free Software Foundation; either version 2 of the License, or -### (at your option) any later version. - -### This program is distributed in the hope that it will be useful, -### but WITHOUT ANY WARRANTY; without even the implied warranty of -### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -### GNU General Public License for more details. - -### You should have received a copy of the GNU General Public License -### along with this program; if not, write to the Free Software -### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -DOWNLOAD_VIDEO = True - -import os -import datetime -import time -import subprocess -import tempfile - -import gtk -import media -import paths -from filmstrip import add_filmstrip - -try: - import kaa.metadata - from hachoir_core.cmd_line import unicodeFilename - from hachoir_parser import createParser - from hachoir_metadata import extractMetadata -except ImportError: - DOWNLOAD_VIDEO = False - -VIDEO_THUMBNAIL_FILE_EXTENSIONS = ['thm'] -VIDEO_FILE_EXTENSIONS = ['3gp', 'avi', 'm2t', 'mov', 'mp4', 'mpeg','mpg', 'mod', 'tod'] - - - -if DOWNLOAD_VIDEO: - - - try: - subprocess.check_call(["ffmpegthumbnailer", "-h"], stdout=subprocess.PIPE) - ffmpeg = True - except: - ffmpeg = False - - - def version_info(): - from hachoir_metadata.version import VERSION - return VERSION - - def get_video_THM_file(fullFileName): - """ - Checks to see if a thumbnail file (THM) is in the same directory as the - file. Expects a full path to be part of the file name. - - Returns the filename, including path, if found, else returns None. - """ - - f = None - name, ext = os.path.splitext(fullFileName) - for e in VIDEO_THUMBNAIL_FILE_EXTENSIONS: - if os.path.exists(name + '.' + e): - f = name + '.' + e - break - if os.path.exists(name + '.' + e.upper()): - f = name + '.' + e.upper() - break - - return f - - class VideoMetaData(): - def __init__(self, filename): - """ - Initialize by loading metadata using hachoir - """ - - self.filename = filename - self.u_filename = unicodeFilename(filename) - self.parser = createParser(self.u_filename, self.filename) - self.metadata = extractMetadata(self.parser) - - - def _kaa_get(self, key, missing, stream=None): - if not hasattr(self, 'info'): - self.info = kaa.metadata.parse(self.filename) - if self.info: - if stream != None: - v = self.info['video'][stream][key] - else: - v = self.info[key] - else: - v = None - if v: - return str(v) - else: - return missing - - def _get(self, key, missing): - try: - v = self.metadata.get(key) - except: - v = missing - return v - - def dateTime(self, missing=''): - return self._get('creation_date', missing) - - def timeStamp(self, missing=''): - """ - Returns a float value representing the time stamp, if it exists - """ - dt = self.dateTime(missing=None) - if dt: - # convert it to a timestamp (not optimal, but better than nothing!) - v = time.mktime(dt.timetuple()) - else: - v = missing - return v - - def codec(self, stream=0, missing=''): - return self._kaa_get('codec', missing, stream) - - def length(self, missing=''): - """ - return the duration (length) of the video, rounded to the nearest second, in string format - """ - delta = self.metadata.get('duration') - l = '%.0f' % (86400 * delta.days + delta.seconds + float('.%s' % delta.microseconds)) - return l - - - def width(self, missing=''): - v = self._get('width', missing) - if v != None: - return str(v) - else: - return None - - def height(self, missing=''): - v = self._get('height', missing) - if v != None: - return str(v) - else: - return None - - def framesPerSecond(self, stream=0, missing=''): - fps = self._kaa_get('fps', missing, stream) - try: - fps = '%.0f' % float(fps) - except: - pass - return fps - - def fourcc(self, stream=0, missing=''): - return self._kaa_get('fourcc', missing, stream) - - def getThumbnailData(self, size, tempWorkingDir): - """ - Returns a pixbuf of the video's thumbnail - - If it cannot be created, returns None - """ - thm = get_video_THM_file(self.filename) - if thm: - thumbnail = gtk.gdk.pixbuf_new_from_file(thm) - aspect = float(thumbnail.get_height()) / thumbnail.get_width() - thumbnail = thumbnail.scale_simple(size, int(aspect*size), gtk.gdk.INTERP_BILINEAR) - thumbnail = add_filmstrip(thumbnail) - else: - if ffmpeg: - try: - tmp = tempfile.NamedTemporaryFile(dir=tempWorkingDir, prefix="rpd-tmp") - tmp.close() - except: - return None - - thm = os.path.join(tempWorkingDir, tmp.name) - - try: - subprocess.check_call(['ffmpegthumbnailer', '-i', self.filename, '-t', '10', '-f', '-o', thm, '-s', str(size)]) - thumbnail = gtk.gdk.pixbuf_new_from_file_at_size(thm, size, size) - os.unlink(thm) - except: - thumbnail = None - else: - thumbnail = None - return thumbnail - -class DummyMetaData(): - """ - Class which gives metadata values for an imaginary video. - - Useful for displaying in preference examples etc. when no video is ready to - be downloaded. - - See VideoMetaData class for documentation of class methods. - """ - def __init__(self): - pass - - def dateTime(self, missing=''): - return datetime.datetime.now() - - def codec(self, stream=0, missing=''): - return 'H.264 AVC' - - def length(self, missing=''): - return '57' - - def width(self, stream=0, missing=''): - return '1920' - - def height(self, stream=0, missing=''): - return '1080' - - def framesPerSecond(self, stream=0, missing=''): - return '24' - - def fourcc(self, stream=0, missing=''): - return 'AVC1' - - -if __name__ == '__main__': - import sys - - - if (len(sys.argv) != 2): - print 'Usage: ' + sys.argv[0] + ' path/to/video/containing/metadata' - sys.exit(0) - - else: - m = VideoMetaData(sys.argv[1]) - dt = m.dateTime() - if dt: - print dt.strftime('%Y%m%d-%H:%M:%S') - print "codec: %s" % m.codec() - print "%s seconds" % m.length() - print "%sx%s" % (m.width(), m.height()) - print "%s fps" % m.framesPerSecond() - print "Fourcc: %s" % (m.fourcc()) - diff --git a/setup.py b/setup.py index 6538751..e9a2005 100644 --- a/setup.py +++ b/setup.py @@ -43,21 +43,21 @@ class InstallData(install_data): return data_files -package_data={'rapid': ['glade3/rapid.glade', +package_data={'rapid': ['glade3/about.ui', + 'glade3/photo.svg', + 'glade3/photo66.png', + 'glade3/photo_icon.png', + 'glade3/prefs.ui', + 'glade3/rapid.ui', 'glade3/rapid-photo-downloader.svg', 'glade3/rapid-photo-downloader-download-pending.svg', 'glade3/rapid-photo-downloader-downloaded-with-error.svg', 'glade3/rapid-photo-downloader-downloaded-with-warning.svg', 'glade3/rapid-photo-downloader-downloaded.svg', 'glade3/rapid-photo-downloader-jobcode.svg', - 'glade3/video.png', - 'glade3/video24.png', - 'glade3/video_small_shadow.png', - 'glade3/video_small.png', - 'glade3/photo.png', - 'glade3/photo_small_shadow.png', - 'glade3/photo_small.png', - 'glade3/photo24.png', + 'glade3/thumbnails_icon.png', + 'glade3/video.svg', + 'glade3/video66.png', 'glade3/zoom-in.png', 'glade3/zoom-out.png', ]} -- cgit v1.2.3