From f49f82b74596eda33ee5e2fc14e51fd67351b9ec Mon Sep 17 00:00:00 2001 From: Julien Valroff Date: Tue, 1 Jun 2010 20:31:41 +0200 Subject: Imported Upstream version 0.2.0 --- rapid/renamesubfolderprefs.py | 283 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 230 insertions(+), 53 deletions(-) (limited to 'rapid/renamesubfolderprefs.py') diff --git a/rapid/renamesubfolderprefs.py b/rapid/renamesubfolderprefs.py index a449fd7..feb7366 100644 --- a/rapid/renamesubfolderprefs.py +++ b/rapid/renamesubfolderprefs.py @@ -77,7 +77,7 @@ 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 change the value in class i18TranslateMeThanks as well!! Thanks!! +# then you MUST change the value in class i18TranslateMeThanks as well!! # *** Level 0 DATE_TIME = 'Date time' @@ -95,12 +95,14 @@ SEPARATOR = os.sep IMAGE_DATE = 'Image date' TODAY = 'Today' YESTERDAY = 'Yesterday' +VIDEO_DATE = 'Video date' # File name NAME_EXTENSION = 'Name + extension' NAME = 'Name' EXTENSION = 'Extension' IMAGE_NUMBER = 'Image number' +VIDEO_NUMBER = 'Video number' # Metadata APERTURE = 'Aperture' @@ -115,6 +117,13 @@ 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' @@ -158,7 +167,8 @@ SEQUENCE_NUMBER_7 = "Seven digits" SUBSECONDS = 'Subseconds' -# ****** note if changing LIST_DATE_TIME_L2, update the default subfolder preference below :D ***** +# ****** 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', @@ -168,7 +178,8 @@ LIST_DATE_TIME_L2 = ['YYYYMMDD', 'YYYY-MM-DD','YYMMDD', 'YY-MM-DD', 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_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 @@ -184,6 +195,7 @@ class i18TranslateMeThanks: # 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: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename @@ -194,6 +206,7 @@ class i18TranslateMeThanks: _('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 @@ -216,6 +229,11 @@ class i18TranslateMeThanks: _('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 @@ -338,6 +356,7 @@ LIST_SHUTTER_COUNT_L2 = [ # Level 1 LIST_DATE_TIME_L1 = [IMAGE_DATE, TODAY, YESTERDAY] +LIST_VIDEO_DATE_TIME_L1 = [VIDEO_DATE, TODAY, YESTERDAY] DICT_DATE_TIME_L1 = { IMAGE_DATE: LIST_IMAGE_DATE_TIME_L2, @@ -345,6 +364,13 @@ DICT_DATE_TIME_L1 = { YESTERDAY: 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, + ORDER_KEY: LIST_VIDEO_DATE_TIME_L1 + } LIST_FILENAME_L1 = [NAME_EXTENSION, NAME, EXTENSION, IMAGE_NUMBER] @@ -357,6 +383,15 @@ DICT_FILENAME_L1 = { 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] @@ -372,7 +407,9 @@ LIST_METADATA_L1 = [APERTURE, ISO, EXPOSURE_TIME, FOCAL_LENGTH, SHORT_CAMERA_MODEL_HYPHEN, SERIAL_NUMBER, SHUTTER_COUNT, - OWNER_NAME] + OWNER_NAME] + +LIST_VIDEO_METADATA_L1 = [CODEC, WIDTH, HEIGHT, LENGTH, FPS] DICT_METADATA_L1 = { APERTURE: None, @@ -388,7 +425,15 @@ DICT_METADATA_L1 = { 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, @@ -412,6 +457,8 @@ DICT_SEQUENCE_L1 = { 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, @@ -422,6 +469,16 @@ DICT_IMAGE_RENAME_L0 = { 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] @@ -434,6 +491,18 @@ DICT_SUBFOLDER_L0 = { 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 @@ -568,7 +637,7 @@ def _upgradePreferencesToCurrent(prefs, previousVersion): return (upgraded, p) -def upgradePreferencesToCurrent(imageRenamePrefs, subfolderPrefs, previousVersion): +def upgradePreferencesToCurrent(imageRenamePrefs, subfolderPrefs, previousVersion): """Upgrades user preferences to current version returns True if the preferences were upgraded""" @@ -585,13 +654,22 @@ def usesJobCode(prefs): return True return False -def checkPreferencesForValidity(imageRenamePrefs, subfolderPrefs, version=config.version): - """Returns true if the passed in preferences are valid""" +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: - checkPreferenceValid(DICT_SUBFOLDER_L0, subfolderPrefs) - checkPreferenceValid(DICT_IMAGE_RENAME_L0, imageRenamePrefs) + 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 @@ -600,6 +678,8 @@ def checkPreferencesForValidity(imageRenamePrefs, subfolderPrefs, version=conf 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 @@ -694,6 +774,10 @@ def filterSubfolderPreferences(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: @@ -718,7 +802,7 @@ class PrefValueInvalidError(PrefKeyError): class PrefLengthError(PrefError): def __init__(self, error): - self.msg = _("These preferences are not well formed:") % self.unpackList(error) + "\n %s" + self.msg = _("These preferences are not well formed:") + "\n %s" % self.unpackList(error) class PrefValueKeyComboError(PrefError): def __init__(self, error): @@ -785,6 +869,7 @@ class ImageRenamePreferences: self.defaultPrefs = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE] self.defaultRow = self.defaultPrefs self.stripForwardSlash = True + self.L1DateCheck = IMAGE_DATE #used in _getDateComponent() @@ -819,19 +904,42 @@ class ImageRenamePreferences: def setJobCode(self, job_code): self.job_code = job_code + + def _fixMangledDateTime(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 _getDateComponent(self): """ - Returns portion of new image / subfolder name based on date time + Returns portion of new image / subfolder name based on date time. + If the date is missing, will attempt to use the fallback date. """ problem = None - if self.L1 == IMAGE_DATE: + + if self.L1 == self.L1DateCheck: if self.L2 == SUBSECONDS: d = self.photo.subSeconds() - problem = _("Subsecond metadata not present in image") + problem = _("Subsecond metadata not present in photo") + if d == '00': + return ('', problem) + else: + return (d, None) else: d = self.photo.dateTime(missing=None) - problem = _("%s metadata is not present in image") % self.L1.lower() + problem = _("%s metadata is not present") % self.L1.lower() + elif self.L1 == TODAY: d = datetime.datetime.now() elif self.L1 == YESTERDAY: @@ -841,34 +949,33 @@ class ImageRenamePreferences: raise("Date options invalid") if d: - if self.L2 <> SUBSECONDS: - - if type(d) == type('string'): - # will be a string only if the date time could not be converted in the datetime type - # try to massage badly formed date / times into a valid value - _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: - v = '' - problem = _('Error in date time component. Value %s appears invalid') % '' - return (v, problem) - + # if format is not the standard floating point representation + # of a date time, there is a problem + if type(d) == type('string'): + # will be a string only if the date time could not be converted in the datetime type + # try to massage badly formed date / times into a valid value + d = self._fixMangledDateTime(d) + if d is None: + v = '' + problem = _('Error in date time component. Value %s appears invalid') % '' + return (v, problem) + else: + if self.fallback_date: try: - return (d.strftime(convertDateForStrftime(self.L2)), None) + d = datetime.datetime.fromtimestamp(self.fallback_date) except: v = '' - problem = _('Error in date time component. Value %s appears invalid') % d - return (v, problem) + problem = _('Error in date time component. Value %s appears invalid') % '' + return (v, problem) else: - return (d, None) - else: - return ('', problem) + return ('', problem) + + try: + return (d.strftime(convertDateForStrftime(self.L2)), None) + except: + v = '' + problem = _('Error in date time component. Value %s appears invalid') % d + return (v, problem) def _getFilenameComponent(self): """ @@ -894,11 +1001,11 @@ class ImageRenamePreferences: filename = extension[1:] else: filename = "" - problem = _("extension was specified but image name has no extension") - elif self.L1 == IMAGE_NUMBER: + problem = _("extension was specified but filename does not have an extension") + elif self.L1 == IMAGE_NUMBER or self.L1 == VIDEO_NUMBER: n = re.search("(?P[0-9]+$)", name) if not n: - problem = _("image number was specified but image filename has no number") + problem = _("image or video number was specified but filename has no number") else: image_number = n.group("image_number") @@ -971,7 +1078,7 @@ class ImageRenamePreferences: md = self.L1.lower() else: md = ISO - problem = _("%s metadata is not present in image") % md + problem = _("%s metadata is not present in photo") % md return (v, problem) @@ -1037,7 +1144,6 @@ class ImageRenamePreferences: def _getDownloadsTodaySequenceNo(self): problem = None - v = self._formatSequenceNo(self.sequences.getDownloadsTodayUsingCounter(self.sequenceCounter), self.L2) return (v, problem) @@ -1085,7 +1191,7 @@ class ImageRenamePreferences: return (os.sep, None) except: v = "" - problem = _("error generating name with component %s") % self.L2 + problem = _("error generating name with component %s") % self.L0 return (v, problem) def _getValuesFromList(self): @@ -1093,10 +1199,11 @@ class ImageRenamePreferences: yield (self.prefList[i], self.prefList[i+1], self.prefList[i+2]) - def _generateName(self, photo, existingFilename, stripCharacters, subfolder, stripInitialPeriodFromExtension, sequence): + def _generateName(self, photo, existingFilename, stripCharacters, subfolder, stripInitialPeriodFromExtension, sequence, fallback_date): self.photo = photo self.existingFilename = existingFilename self.stripInitialPeriodFromExtension = stripInitialPeriodFromExtension + self.fallback_date = fallback_date name = '' problem = '' @@ -1130,7 +1237,8 @@ class ImageRenamePreferences: stripCharacters = False, subfolder=None, stripInitialPeriodFromExtension=False, sequencesPreliminary = True, - sequence_to_use = None): + sequence_to_use = None, + fallback_date = None): """ Generate a filename for the photo in string format based on user prefs. @@ -1150,7 +1258,7 @@ class ImageRenamePreferences: sequence = 0 return self._generateName(photo, existingFilename, stripCharacters, subfolder, - stripInitialPeriodFromExtension, sequence) + stripInitialPeriodFromExtension, sequence, fallback_date) def generateNameSequencePossibilities(self, photo, existingFilename, stripCharacters=False, subfolder=None, @@ -1312,16 +1420,65 @@ class ImageRenamePreferences: 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.photo.codec() + elif video.L1 == WIDTH: + v = video.photo.width() + elif video.L1 == HEIGHT: + v = video.photo.height() + elif video.L1 == FPS: + v = video.photo.fps() + elif video.L1 == LENGTH: + v = video.photo.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: + problem = _("%s metadata is not present in video") % video.L1 + return (v, problem) + +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 + 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 ImageRenamePreferences.__init__(self, prefList, parent) def generateNameUsingPreferences(self, photo, existingFilename=None, - stripCharacters = False): + stripCharacters = False, fallback_date = None): """ Generate a filename for the photo in string format based on user prefs. @@ -1332,7 +1489,9 @@ class SubfolderPreferences(ImageRenamePreferences): subfolders, problem = ImageRenamePreferences.generateNameUsingPreferences( self, photo, - existingFilename, stripCharacters, stripInitialPeriodFromExtension=True) + 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 @@ -1394,11 +1553,29 @@ class SubfolderPreferences(ImageRenamePreferences): return v -class Sequences: - """ Holds sequence numbers and letters used in generating filenames""" - def __init__(self, downloadsToday, storedSequenceNo): +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 + 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 -- cgit v1.2.3