1#!/usr/bin/env python3
2
3# Copyright (C) 2007-2020 Damon Lynch <damonlynch@gmail.com>
4
5# This file is part of Rapid Photo Downloader.
6#
7# Rapid Photo Downloader is free software: you can redistribute it and/or
8# modify it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# Rapid Photo Downloader is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Rapid Photo Downloader.  If not,
19# see <http://www.gnu.org/licenses/>.
20
21# Special key in each dictionary which specifies the order of elements.
22# It is very important to have a consistent and rational order when displaying
23# these prefs to the user, and dictionaries are unsorted.
24
25__author__ = 'Damon Lynch'
26__copyright__ = "Copyright 2007-2020, Damon Lynch"
27
28import os
29from collections import OrderedDict
30from typing import List, Optional, Tuple
31
32
33# PLEASE NOTE: these values are duplicated in a dummy class whose function
34# is to have them put into the translation template. If you change the values below
35# then you MUST change the value in class i18TranslateMeThanks as well!!
36
37# *** Level 0, i.e. first column of values presented to user
38DATE_TIME = 'Date time'
39TEXT = 'Text'
40FILENAME = 'Filename'
41METADATA = 'Metadata'
42SEQUENCES = 'Sequences'
43JOB_CODE = 'Job code'
44
45SEPARATOR = os.sep
46
47# *** Level 1, i.e. second column of values presented to user
48
49# Date time
50IMAGE_DATE = 'Image date'
51TODAY = 'Today'
52YESTERDAY = 'Yesterday'
53VIDEO_DATE = 'Video date'
54DOWNLOAD_TIME = 'Download time'
55
56# File name
57NAME = 'Name'
58IMAGE_NUMBER = 'Image number'
59VIDEO_NUMBER = 'Video number'
60
61# pre 0.9.0a4 File name values: NAME_EXTENSION, EXTENSION
62NAME_EXTENSION = 'Name + extension'
63
64# however extension is used for subfolder generation in all versions
65EXTENSION = 'Extension'
66
67
68# Metadata
69APERTURE = 'Aperture'
70ISO = 'ISO'
71EXPOSURE_TIME = 'Exposure time'
72FOCAL_LENGTH = 'Focal length'
73CAMERA_MAKE = 'Camera make'
74CAMERA_MODEL = 'Camera model'
75SHORT_CAMERA_MODEL = 'Short camera model'
76SHORT_CAMERA_MODEL_HYPHEN = 'Hyphenated short camera model'
77SERIAL_NUMBER = 'Serial number'
78SHUTTER_COUNT = 'Shutter count'
79# Currently the only file number is Exif.CanonFi.FileNumber,
80# which is in the format xxx-yyyy, where xxx is the folder and yyyy the image
81FILE_NUMBER = 'File number'
82OWNER_NAME = 'Owner name'
83COPYRIGHT = 'Copyright'
84ARTIST = 'Artist'
85
86# Video metadata
87CODEC = 'Codec'
88WIDTH = 'Width'
89HEIGHT = 'Height'
90FPS = 'Frames Per Second'
91LENGTH = 'Length'
92
93# Image sequences
94DOWNLOAD_SEQ_NUMBER = 'Downloads today'
95SESSION_SEQ_NUMBER = 'Session number'
96SUBFOLDER_SEQ_NUMBER = 'Subfolder number'
97STORED_SEQ_NUMBER = 'Stored number'
98SEQUENCE_LETTER = 'Sequence letter'
99
100# *** Level 2, i.e. third and final column of values presented to user
101
102# Image number
103IMAGE_NUMBER_ALL = 'All digits'
104IMAGE_NUMBER_1 = 'Last digit'
105IMAGE_NUMBER_2 = 'Last 2 digits'
106IMAGE_NUMBER_3 = 'Last 3 digits'
107IMAGE_NUMBER_4 = 'Last 4 digits'
108
109# Case
110ORIGINAL_CASE = "Original Case"
111UPPERCASE = "UPPERCASE"
112LOWERCASE = "lowercase"
113
114# Sequence number
115SEQUENCE_NUMBER_1 = "One digit"
116SEQUENCE_NUMBER_2 = "Two digits"
117SEQUENCE_NUMBER_3 = "Three digits"
118SEQUENCE_NUMBER_4 = "Four digits"
119SEQUENCE_NUMBER_5 = "Five digits"
120SEQUENCE_NUMBER_6 = "Six digits"
121SEQUENCE_NUMBER_7 = "Seven digits"
122
123# File number
124FILE_NUMBER_FOLDER = "Folder only"
125FILE_NUMBER_ALL = "Folder and file"
126
127# Now, define dictionaries and lists of valid combinations of preferences.
128
129# Level 2
130
131# Date
132
133SUBSECONDS = 'Subseconds'
134
135# NOTE 1: if changing LIST_DATE_TIME_L2, you MUST update the default
136# subfolder preference immediately below
137# NOTE 2: if changing LIST_DATE_TIME_L2, you MUST also update
138# DATE_TIME_CONVERT below
139# NOTE 3: if changing LIST_DATE_TIME_L2, you MUST also update
140# PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV
141LIST_DATE_TIME_L2 = [
142    'YYYYMMDD',  # 0
143    'YYYY-MM-DD',
144    'YYYY_MM_DD',  # 2
145    'YYMMDD',
146    'YY-MM-DD',  # 4
147    'YY_MM_DD',
148    'MMDDYYYY',  # 6
149    'MMDDYY',
150    'MMDD',  # 8
151    'DDMMYYYY',
152    'DDMMYY',  # 10
153    'YYYY',
154    'YY',  # 12
155    'MM',
156    'DD',  # 14
157    'Month (full)',
158    'Month (abbreviated)',  # 16
159    'Weekday (full)',
160    'Weekday (abbreviated)', # 18
161    'HHMMSS',
162    'HHMM',  # 20
163    'HH-MM-SS',
164    'HH-MM',  # 22
165    'HH',
166    'MM (minutes)',  # 24
167    'SS'
168]
169
170LIST_IMAGE_DATE_TIME_L2 = LIST_DATE_TIME_L2 + [SUBSECONDS]
171
172DEFAULT_SUBFOLDER_PREFS = [
173    DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11], '/', '', '', DATE_TIME, IMAGE_DATE,
174    LIST_DATE_TIME_L2[0]
175]
176DEFAULT_VIDEO_SUBFOLDER_PREFS = [
177    DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11], '/', '', '', DATE_TIME,
178    VIDEO_DATE, LIST_DATE_TIME_L2[0]
179]
180
181DEFAULT_PHOTO_RENAME_PREFS = [FILENAME, NAME, ORIGINAL_CASE]
182DEFAULT_VIDEO_RENAME_PREFS = [FILENAME, NAME, ORIGINAL_CASE]
183
184
185class i18TranslateMeThanks:
186    """ this class is never used in actual running code
187    Its purpose is to have these values inserted into the program's i18n template file
188
189    """
190
191    def __init__(self):
192        _('Date time')
193        _('Text')
194        _('Filename')
195        _('Metadata')
196        _('Sequences')
197        # Translators: for an explanation of what this means,
198        # see http://damonlynch.net/rapid/documentation/index.html#jobcode
199        _('Job code')
200        _('Image date')
201        _('Video date')
202        _('Today')
203        _('Yesterday')
204        # Translators: Download time is the time and date that the download started (when the
205        # user clicked the Download button)
206        _('Download time')
207        # Translators: for an explanation of what this means,
208        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
209        _('Name')
210        # Translators: for an explanation of what this means,
211        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
212        _('Extension')
213        # Translators: for an explanation of what this means,
214        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
215        _('Image number')
216        _('Video number')
217        # Translators: for an explanation of what this means,
218        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
219        _('Aperture')
220        # Translators: for an explanation of what this means,
221        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
222        _('ISO')
223        # Translators: for an explanation of what this means,
224        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
225        _('Exposure time')
226        # Translators: for an explanation of what this means,
227        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
228        _('Focal length')
229        # Translators: for an explanation of what this means,
230        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
231        _('Camera make')
232        # Translators: for an explanation of what this means,
233        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
234        _('Camera model')
235        # Translators: for an explanation of what this means,
236        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
237        _('Short camera model')
238        # Translators: for an explanation of what this means,
239        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
240        _('Hyphenated short camera model')
241        # Translators: for an explanation of what this means,
242        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
243        _('Serial number')
244        # Translators: for an explanation of what this means,
245        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
246        _('Shutter count')
247        # File number currently refers to the Exif value Exif.Canon.FileNumber
248        _('File number')
249        # Only the folder component of the Exif.Canon.FileNumber value
250        _('Folder only')
251        # The folder and file component of the Exif.Canon.FileNumber value
252        _('Folder and file')
253        # Translators: for an explanation of what this means,
254        # see http://damonlynch.net/rapid/documentation/index.html#renamemetadata
255        _('Owner name')
256        _('Codec')
257        _('Width')
258        _('Height')
259        _('Length')
260        _('Frames Per Second')
261        _('Artist')
262        _('Copyright')
263        # Translators: for an explanation of what this means,
264        # see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers
265        _('Downloads today')
266        # Translators: for an explanation of what this means,
267        # see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers
268        _('Session number')
269        # Translators: for an explanation of what this means,
270        # see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers
271        _('Subfolder number')
272        # Translators: for an explanation of what this means,
273        # see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers
274        _('Stored number')
275        # Translators: for an explanation of what this means,
276        # see http://damonlynch.net/rapid/documentation/index.html#sequenceletters
277        _('Sequence letter')
278        # Translators: for an explanation of what this means,
279        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
280        _('All digits')
281        # Translators: for an explanation of what this means,
282        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
283        _('Last digit')
284        # Translators: for an explanation of what this means,
285        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
286        _('Last 2 digits')
287        # Translators: for an explanation of what this means,
288        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
289        _('Last 3 digits')
290        # Translators: for an explanation of what this means,
291        # see http://damonlynch.net/rapid/documentation/index.html#renamefilename
292        _('Last 4 digits')
293        # Translators: please not the capitalization of this text, and keep it the same if your
294        # language features capitalization
295        _("Original Case")
296        # Translators: please not the capitalization of this text, and keep it the same if your
297        # language features capitalization
298        _("UPPERCASE")
299        # Translators: please not the capitalization of this text, and keep it the same if your
300        # language features capitalization
301        _("lowercase")
302        _("One digit")
303        _("Two digits")
304        _("Three digits")
305        _("Four digits")
306        _("Five digits")
307        _("Six digits")
308        _("Seven digits")
309        # Translators: for an explanation of what this means,
310        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
311        _('Subseconds')
312        # Translators: for an explanation of what this means,
313        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
314        _('YYYYMMDD')
315        # Translators: for an explanation of what this means,
316        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
317        _('YYYY-MM-DD')
318        # Translators: for an explanation of what this means,
319        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
320        _('YYYY_MM_DD')
321        # Translators: for an explanation of what this means,
322        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
323        _('YYMMDD')
324        # Translators: for an explanation of what this means,
325        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
326        _('YY-MM-DD')
327        # Translators: for an explanation of what this means,
328        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
329        _('YY_MM_DD')
330        # Translators: for an explanation of what this means,
331        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
332        _('MMDDYYYY')
333        # Translators: for an explanation of what this means,
334        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
335        _('MMDDYY')
336        # Translators: for an explanation of what this means,
337        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
338        _('MMDD')
339        # Translators: for an explanation of what this means,
340        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
341        _('DDMMYYYY')
342        # Translators: for an explanation of what this means,
343        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
344        _('DDMMYY')
345        # Translators: for an explanation of what this means,
346        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
347        _('YYYY')
348        # Translators: for an explanation of what this means,
349        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
350        _('YY')
351        # Translators: for an explanation of what this means,
352        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
353        _('MM')
354        # Translators: for an explanation of what this means,
355        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
356        _('DD')
357        # Translators: for an explanation of what this means,
358        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
359        _('Month (full)'),
360        # Translators: for an explanation of what this means,
361        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
362        _('Month (abbreviated)'),
363        # Translators: for an explanation of what this means,
364        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
365        _('Weekday (full)')
366        # Translators: for an explanation of what this means,
367        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
368        _('Weekday (abbreviated)')
369        # Translators: for an explanation of what this means,
370        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
371        _('HHMMSS')
372        # Translators: for an explanation of what this means,
373        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
374        _('HHMM')
375        # Translators: for an explanation of what this means,
376        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
377        _('HH-MM-SS')
378        # Translators: for an explanation of what this means,
379        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
380        _('HH-MM')
381        # Translators: for an explanation of what this means,
382        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
383        _('HH')
384        # Translators: for an explanation of what this means,
385        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
386        _('MM (minutes)')
387        # Translators: for an explanation of what this means,
388        # see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
389        _('SS')
390
391    # Convenience values for python datetime conversion using values in
392
393
394# Default subfolder options that appear in drop-down menu in Destination views
395# Any change to PHOTO_SUBFOLDER_MENU_DEFAULTS must also be reflected in
396# PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV
397
398# The following values will be displayed in the menu after an os.sep.join() operation
399
400PHOTO_SUBFOLDER_MENU_DEFAULTS = (
401    (_('Date'), _('YYYY'), _('YYYYMMDD')),
402    (_('Date (hyphens)'), _('YYYY'), _('YYYY-MM-DD')),
403    (_('Date (underscores)'), _('YYYY'), _('YYYY_MM_DD')),
404    (_('Date and Job Code'), _('YYYY'), _('YYYYMM_Job Code')),
405    (_('Date and Job Code Subfolder'), _('YYYY'), _('YYYYMM'), _('Job Code'))
406)
407
408# Any change to PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV must also be reflected in
409# PHOTO_SUBFOLDER_MENU_DEFAULTS
410
411PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV = (
412    # 0
413    [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11],
414     '/', '', '',
415     DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0]
416    ],
417    # 1
418    [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11],
419     '/', '', '',
420     DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[1]
421    ],
422    # 2
423    [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11],
424     '/', '', '',
425     DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[2]
426    ],
427    # 3
428    [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11],
429     '/', '', '',
430     DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11],
431     DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[13],
432     TEXT, '_', '',
433     JOB_CODE, '', ''],
434    # 4
435    [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11],
436     '/', '', '',
437     DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11],
438     DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[13],
439     '/', '', '',
440     JOB_CODE, '', '',
441     ],
442)
443
444PHOTO_RENAME_MENU_DEFAULTS = (
445    (_('Original Filename'), 'IMG_1234'),
446    (_('Date-Time and Downloads today'), _('YYYYMMDD-HHMM-1')),
447    (_('Date and Downloads today'), _('YYYYMMDD-1')),
448    (_('Date-Time and Image number'), _('YYYYMMDD-1234')),
449    (_('Date-Time and Job Code'), _('YYYYMMDD-HHMM-Job Code-1')),
450    (_('Date and Job Code'), _('YYYYMMDD-Job Code-1'))
451)
452
453PHOTO_RENAME_MENU_DEFAULTS_CONV = (
454    # 0 Original Filename
455    [FILENAME, NAME, ORIGINAL_CASE],
456    # 1 Date-Time and Downloads today
457    [
458        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0],
459        TEXT, '-', '',
460        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[20],
461        TEXT, '-', '',
462        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
463    ],
464    # 2 Date and Downloads today
465    [
466        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0],
467        TEXT, '-', '',
468        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
469    ],
470    # 3 Date-Time and Image number
471    [
472        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0],
473        TEXT, '-', '',
474        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[20],
475        TEXT, '-', '',
476        FILENAME, IMAGE_NUMBER, IMAGE_NUMBER_ALL
477    ],
478    # 4 Date-Time and Job Code
479    [
480        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0],
481        TEXT, '-', '',
482        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[20],
483        TEXT, '-', '',
484        JOB_CODE, '', '',
485        TEXT, '-', '',
486        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
487    ],
488    # 5 Date and Job Code
489    [
490        DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0],
491        TEXT, '-', '',
492        JOB_CODE, '', '',
493        TEXT, '-', '',
494        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
495    ]
496)
497
498# See notes above regarding keeping values in sync
499VIDEO_SUBFOLDER_MENU_DEFAULTS = PHOTO_SUBFOLDER_MENU_DEFAULTS
500VIDEO_SUBFOLDER_MENU_DEFAULTS_CONV = (
501    # 0
502    [
503        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11],
504        SEPARATOR, '', '',
505        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0]
506     ],
507    # 1
508    [
509        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11],
510        SEPARATOR, '', '',
511        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[1]
512     ],
513    # 2
514    [
515        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11],
516        SEPARATOR, '', '',
517        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[2]
518     ],
519    # 3
520    [
521        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11],
522        SEPARATOR, '', '',
523        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11],
524        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[13],
525        TEXT, '_', '',
526        JOB_CODE, '', ''
527    ],
528    # 4
529    [
530        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11],
531        SEPARATOR, '', '',
532        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[11],
533        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[13],
534        SEPARATOR, '', '',
535        JOB_CODE, '', '',
536     ],
537)
538
539VIDEO_RENAME_MENU_DEFAULTS = (
540    (_('Original Filename'), 'MVI_1234'),
541    (_('Date-Time and Downloads today'), _('YYYYMMDD-HHMM-1')),
542    (_('Date and Downloads today'), _('YYYYMMDD-1')),
543    (_('Date-Time and Video number'), _('YYYYMMDD_1234')),
544    (_('Date-Time and Job Code'), _('YYYYMMDD-HHMM-Job Code-1')),
545    (_('Date and Job Code'), _('YYYYMMDD-Job Code-1')),
546    (_('Resolution'), _('YYYYMMDD-HHMM-1-1920x1080'))
547)
548
549VIDEO_RENAME_MENU_DEFAULTS_CONV = (
550    # 0 Original Filename
551    [FILENAME, NAME, ORIGINAL_CASE],
552    # 1 Date-Time and Downloads today
553    [
554        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0],
555        TEXT, '-', '',
556        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[20],
557        TEXT, '-', '',
558        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
559    ],
560    # 2 Date and Downloads today
561    [
562        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0],
563        TEXT, '-', '',
564        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
565    ],
566    # 3 Date-Time and Image number
567    [
568        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0],
569        TEXT, '-', '',
570        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[20],
571        TEXT, '-', '',
572        FILENAME, VIDEO_NUMBER, IMAGE_NUMBER_ALL
573    ],
574    # 4 Date-Time and Job Code
575    [
576        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0],
577        TEXT, '-', '',
578        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[20],
579        TEXT, '-', '',
580        JOB_CODE, '', '',
581        TEXT, '-', '',
582        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
583    ],
584    # 5 Date and Job Code
585    [
586        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0],
587        TEXT, '-', '',
588        JOB_CODE, '', '',
589        TEXT, '-', '',
590        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1
591    ],
592    # 6 Resolution
593    [
594        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[0],
595        TEXT, '-', '',
596        DATE_TIME, VIDEO_DATE, LIST_DATE_TIME_L2[20],
597        TEXT, '-', '',
598        SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1,
599        TEXT, '-', '',
600        METADATA, WIDTH, '',
601        TEXT, 'x', '',
602        METADATA, HEIGHT, ''
603    ]
604)
605
606# See notes above regarding keeping values in sync
607DATE_TIME_CONVERT = [
608    '%Y%m%d',  # 0
609    '%Y-%m-%d',
610    '%Y_%m_%d',  # 2
611    '%y%m%d',
612    '%y-%m-%d',   # 4
613    '%y_%m_%d',
614    '%m%d%Y',  # 6
615    '%m%d%y',
616    '%m%d',  # 8
617    '%d%m%Y',
618    '%d%m%y', # 10
619    '%Y',
620    '%y',  # 12
621    '%m',
622    '%d',  # 14
623    '%B',
624    '%b',  # 16
625    '%A',
626    '%a',     # 18
627    '%H%M%S',
628    '%H%M',  # 20
629    '%H-%M-%S',
630    '%H-%M',  # 22
631    '%H',
632    '%M',  # 24
633    '%S'
634]
635
636LIST_IMAGE_NUMBER_L2 = [
637    IMAGE_NUMBER_ALL, IMAGE_NUMBER_1, IMAGE_NUMBER_2, IMAGE_NUMBER_3, IMAGE_NUMBER_4
638]
639
640LIST_CASE_L2 = [ORIGINAL_CASE, UPPERCASE, LOWERCASE]
641
642LIST_SEQUENCE_LETTER_L2 = [
643    UPPERCASE,
644    LOWERCASE
645]
646
647LIST_SEQUENCE_NUMBERS_L2 = [
648    SEQUENCE_NUMBER_1,
649    SEQUENCE_NUMBER_2,
650    SEQUENCE_NUMBER_3,
651    SEQUENCE_NUMBER_4,
652    SEQUENCE_NUMBER_5,
653    SEQUENCE_NUMBER_6,
654    SEQUENCE_NUMBER_7,
655]
656
657LIST_SHUTTER_COUNT_L2 = [
658    SEQUENCE_NUMBER_3,
659    SEQUENCE_NUMBER_4,
660    SEQUENCE_NUMBER_5,
661    SEQUENCE_NUMBER_6,
662]
663FILE_NUMBER_L2 = [
664    FILE_NUMBER_FOLDER,
665    FILE_NUMBER_ALL
666]
667
668# Level 1
669
670DICT_DATE_TIME_L1 = OrderedDict(
671    [
672        (IMAGE_DATE, LIST_IMAGE_DATE_TIME_L2),
673        (TODAY, LIST_DATE_TIME_L2),
674        (YESTERDAY, LIST_DATE_TIME_L2),
675        (DOWNLOAD_TIME, LIST_DATE_TIME_L2),
676    ]
677)
678
679VIDEO_DICT_DATE_TIME_L1 = OrderedDict(
680    [
681        (VIDEO_DATE, LIST_IMAGE_DATE_TIME_L2),
682        (TODAY, LIST_DATE_TIME_L2),
683        (YESTERDAY, LIST_DATE_TIME_L2),
684        (DOWNLOAD_TIME, LIST_DATE_TIME_L2),
685    ]
686)
687
688DICT_FILENAME_L1 = OrderedDict(
689    [
690        (NAME, LIST_CASE_L2),
691        (IMAGE_NUMBER, LIST_IMAGE_NUMBER_L2),
692    ]
693)
694
695# pre 0.9.0a4 values for DICT_FILENAME_L1:
696#(NAME_EXTENSION, LIST_CASE_L2),
697# (EXTENSION, LIST_CASE_L2),
698
699DICT_VIDEO_FILENAME_L1 = OrderedDict(
700    [
701        (NAME, LIST_CASE_L2),
702        (VIDEO_NUMBER, LIST_IMAGE_NUMBER_L2),
703    ]
704)
705
706# pre 0.9.0a4 values for DICT_VIDEO_FILENAME_L1:
707# (NAME_EXTENSION, LIST_CASE_L2),
708# (EXTENSION, LIST_CASE_L2),
709
710DICT_SUBFOLDER_FILENAME_L1 = {
711    EXTENSION: LIST_CASE_L2,
712}
713
714DICT_METADATA_L1 = OrderedDict(
715    [
716        (APERTURE, None),
717        (ISO, None),
718        (EXPOSURE_TIME, None),
719        (FOCAL_LENGTH, None),
720        (CAMERA_MAKE, LIST_CASE_L2),
721        (CAMERA_MODEL, LIST_CASE_L2),
722        (SHORT_CAMERA_MODEL, LIST_CASE_L2),
723        (SHORT_CAMERA_MODEL_HYPHEN, LIST_CASE_L2),
724        (SERIAL_NUMBER, None),
725        (SHUTTER_COUNT, LIST_SHUTTER_COUNT_L2),
726        (FILE_NUMBER, FILE_NUMBER_L2),
727        (OWNER_NAME, LIST_CASE_L2),
728        (ARTIST, LIST_CASE_L2),
729        (COPYRIGHT, LIST_CASE_L2),
730    ]
731)
732
733DICT_VIDEO_METADATA_L1 = OrderedDict(
734    [
735        (CODEC, LIST_CASE_L2),
736        (WIDTH, None),
737        (HEIGHT, None),
738        (LENGTH, None),
739        (FPS, None),
740    ]
741)
742
743DICT_SEQUENCE_L1 = OrderedDict(
744    [
745        (DOWNLOAD_SEQ_NUMBER, LIST_SEQUENCE_NUMBERS_L2),
746        (STORED_SEQ_NUMBER, LIST_SEQUENCE_NUMBERS_L2),
747        (SESSION_SEQ_NUMBER, LIST_SEQUENCE_NUMBERS_L2),
748        (SEQUENCE_LETTER, LIST_SEQUENCE_LETTER_L2),
749    ]
750)
751
752LIST_SEQUENCE_L1 = list(DICT_SEQUENCE_L1.keys())
753
754# Level 0
755
756DICT_IMAGE_RENAME_L0 = OrderedDict(
757    [
758        (DATE_TIME, DICT_DATE_TIME_L1),
759        (TEXT, None),
760        (FILENAME, DICT_FILENAME_L1),
761        (METADATA, DICT_METADATA_L1),
762        (SEQUENCES, DICT_SEQUENCE_L1),
763        (JOB_CODE, None),
764    ]
765)
766
767DICT_VIDEO_RENAME_L0 = OrderedDict(
768    [
769        (DATE_TIME, VIDEO_DICT_DATE_TIME_L1),
770        (TEXT, None),
771        (FILENAME, DICT_VIDEO_FILENAME_L1),
772        (METADATA, DICT_VIDEO_METADATA_L1),
773        (SEQUENCES, DICT_SEQUENCE_L1),
774        (JOB_CODE, None),
775    ]
776)
777
778DICT_SUBFOLDER_L0 = OrderedDict(
779    [
780        (DATE_TIME, DICT_DATE_TIME_L1),
781        (TEXT, None),
782        (FILENAME, DICT_SUBFOLDER_FILENAME_L1),
783        (METADATA, DICT_METADATA_L1),
784        (SEPARATOR, None),
785        (JOB_CODE, None),
786    ]
787)
788
789DICT_VIDEO_SUBFOLDER_L0 = OrderedDict(
790    [
791        (DATE_TIME, VIDEO_DICT_DATE_TIME_L1),
792        (TEXT, None),
793        (FILENAME, DICT_SUBFOLDER_FILENAME_L1),
794        (METADATA, DICT_VIDEO_METADATA_L1),
795        (SEPARATOR, None),
796        (JOB_CODE, None),
797    ]
798)
799
800# preference elements that require metadata
801# note there is no need to specify lower level elements if a higher level
802# element is necessary for them to be present to begin with
803METADATA_ELEMENTS = [METADATA, IMAGE_DATE]
804
805# preference elements that are sequence numbers or letters
806SEQUENCE_ELEMENTS = [
807    DOWNLOAD_SEQ_NUMBER,
808    SESSION_SEQ_NUMBER,
809    SUBFOLDER_SEQ_NUMBER,
810    STORED_SEQ_NUMBER,
811    SEQUENCE_LETTER
812]
813
814# preference elements that do not require metadata and are not fixed
815# as above, there is no need to specify lower level elements if a higher level
816# element is necessary for them to be present to begin with
817DYNAMIC_NON_METADATA_ELEMENTS = [TODAY, YESTERDAY, FILENAME] + SEQUENCE_ELEMENTS
818
819PHOTO_RENAME_COMPLEX = [
820    'Date time', 'Image date', 'YYYYMMDD', 'Text', '-', '', 'Date time', 'Image date', 'HHMM',
821    'Text', '-', '', 'Sequences', 'Downloads today', 'One digit', 'Text', '-iso', '', 'Metadata',
822    'ISO', '', 'Text', '-f', '', 'Metadata', 'Aperture', '', 'Text', '-', '', 'Metadata',
823    'Focal length', '', 'Text', 'mm-', '', 'Metadata', 'Exposure time', ''
824]
825PHOTO_RENAME_SIMPLE = [
826    'Date time', 'Image date', 'YYYYMMDD', 'Text', '-', '', 'Date time', 'Image date', 'HHMM',
827    'Text', '-', '', 'Sequences', 'Downloads today', 'One digit'
828]
829
830VIDEO_RENAME_SIMPLE = [x if x != 'Image date' else 'Video date' for x in PHOTO_RENAME_SIMPLE]
831
832JOB_CODE_RENAME_TEST = ['Job code', '', '', 'Sequences', 'Downloads today', 'One digit']
833
834
835def upgrade_pre090a4_rename_pref(pref_list: List[str]) -> Tuple[List[str], str]:
836    r"""
837    Upgrade photo and video rename preference list
838
839    :param pref_list: pref list to upgrade
840    :return: tuple of new pref list, and if found, the case to be used for the
841     extension
842
843    >>> upgrade_pre090a4_rename_pref([FILENAME, NAME_EXTENSION, ORIGINAL_CASE])
844    (['Filename', 'Name', 'Original Case'], 'Original Case')
845    >>> upgrade_pre090a4_rename_pref(PHOTO_RENAME_SIMPLE + [FILENAME, EXTENSION, LOWERCASE])
846    ... # doctest: +NORMALIZE_WHITESPACE
847    (['Date time', 'Image date', 'YYYYMMDD',
848      'Text', '-', '',
849      'Date time', 'Image date', 'HHMM',
850      'Text', '-', '',
851      'Sequences', 'Downloads today', 'One digit'], 'lowercase')
852    >>> upgrade_pre090a4_rename_pref(PHOTO_RENAME_COMPLEX + [FILENAME, EXTENSION, UPPERCASE])
853    ... # doctest: +NORMALIZE_WHITESPACE
854    (['Date time', 'Image date', 'YYYYMMDD', 'Text', '-', '',
855      'Date time', 'Image date', 'HHMM', 'Text', '-', '', 'Sequences',
856      'Downloads today', 'One digit', 'Text', '-iso', '',
857      'Metadata', 'ISO', '', 'Text', '-f', '', 'Metadata',
858      'Aperture', '', 'Text', '-', '', 'Metadata', 'Focal length', '',
859      'Text', 'mm-', '', 'Metadata', 'Exposure time', ''], 'UPPERCASE')
860     >>> upgrade_pre090a4_rename_pref([FILENAME, NAME, LOWERCASE])
861     (['Filename', 'Name', 'lowercase'], None)
862
863    """
864    if not pref_list:
865        return (pref_list, None)
866
867    # get extension case from last value
868    if pref_list[-2] in (NAME_EXTENSION, EXTENSION):
869        case = pref_list[-1]
870    else:
871        case = None
872
873    new_pref_list = []
874    for idx in range(0, len(pref_list), 3):
875        l1 = pref_list[idx + 1]
876        if  l1 != EXTENSION:
877            if l1 == NAME_EXTENSION:
878                l1 = NAME
879            new_pref_list.extend([pref_list[idx], l1, pref_list[idx + 2]])
880    return new_pref_list, case
881
882
883class PrefError(Exception):
884    """ base class """
885
886    def __init__(self):
887        super().__init__()
888        self.msg = ''
889
890    def unpackList(self, l: List[str]):
891        """
892        Make the preferences presentable to the user
893        """
894        return ', '.join("'{}'".format(i) for i in l)
895
896    def __str__(self):
897        return self.msg
898
899
900class PrefKeyError(PrefError):
901    def __init__(self, error):
902        super().__init__()
903        value = error[0]
904        expectedValues = self.unpackList(error[1])
905        self.msg = "Preference key '%(key)s' is invalid.\nExpected one of %(value)s" % {
906            'key': value, 'value': expectedValues}
907
908
909class PrefValueInvalidError(PrefKeyError):
910    def __init__(self, error):
911        super().__init__(error)
912        value = error[0]
913        self.msg = "Preference value '%(value)s' is invalid" % {'value': value}
914
915
916class PrefLengthError(PrefError):
917    def __init__(self, error):
918        super().__init__()
919        self.msg = "These preferences are not well formed:" + "\n %s" % self.unpackList(error)
920
921
922class PrefValueKeyComboError(PrefError):
923    def __init__(self, error):
924        super().__init__()
925        self.msg = error
926
927
928def check_pref_valid(pref_defn, prefs, modulo=3) -> bool:
929    """
930    Checks to see if user preferences are valid according to their
931    definition. Raises appropriate exception if an error is found.
932
933    :param prefs: list of preferences
934    :param pref_defn: is a Dict specifying what is valid
935    :param modulo: how many list elements are equivalent to one line
936    of preferences.
937    :return: True if prefs match with pref_defn
938    """
939
940    if (len(prefs) % modulo != 0) or not prefs:
941        raise PrefLengthError(prefs)
942    else:
943        for i in range(0, len(prefs), modulo):
944            _check_pref_valid(pref_defn, prefs[i:i + modulo])
945
946    return True
947
948
949def _check_pref_valid(pref_defn, prefs):
950    key = prefs[0]
951    value = prefs[1]
952
953    if key in pref_defn:
954
955        next_pref_defn = pref_defn[key]
956
957        if value is None:
958            # value should never be None, at any time
959            raise PrefValueInvalidError((None, next_pref_defn))
960
961        if next_pref_defn and not value:
962            raise PrefValueInvalidError((value, next_pref_defn))
963
964        if isinstance(next_pref_defn, dict):
965            return _check_pref_valid(next_pref_defn, prefs[1:])
966        else:
967            if isinstance(next_pref_defn, list):
968                result = value in next_pref_defn
969                if not result:
970                    raise PrefValueInvalidError((value, next_pref_defn))
971                return True
972            elif not next_pref_defn:
973                return True
974            else:
975                result = next_pref_defn == value
976                if not result:
977                    raise PrefValueInvalidError((value, next_pref_defn))
978                return True
979    else:
980        raise PrefKeyError((key, list(pref_defn.keys())))
981
982
983def filter_subfolder_prefs(pref_list: List[str],
984                           pref_colors: Optional[List[str]]=None) \
985        -> Tuple[bool, List[str], Optional[List[str]]]:
986    """
987    Filters out extraneous preference choices.
988
989    :param pref_list: the list of user specified preferences
990    :param pref_colors: optional list of colors associated with displaying the
991     generated sample name while editing the preferences
992    :return: bool indicating whether list changed, the pref list, and optionally the
993     list of colors
994    """
995
996    prefs_changed = False
997    continue_check = True
998    while continue_check and pref_list:
999        continue_check = False
1000        if pref_list[0] == SEPARATOR:
1001            # subfolder preferences should not start with a /
1002            pref_list = pref_list[3:]
1003            if pref_colors is not None:
1004                pref_colors = pref_colors[1:]
1005            prefs_changed = True
1006            continue_check = True
1007        elif pref_list[-3] == SEPARATOR:
1008            # subfolder preferences should not end with a /
1009            pref_list = pref_list[:-3]
1010            if pref_colors is not None:
1011                pref_colors = pref_colors[:-1]
1012            continue_check = True
1013            prefs_changed = True
1014        else:
1015            for i in range(0, len(pref_list) - 3, 3):
1016                if pref_list[i] == SEPARATOR and pref_list[i + 3] == SEPARATOR:
1017                    # subfolder preferences should not contain two /s side by side
1018                    continue_check = True
1019                    prefs_changed = True
1020                    # note we are messing with the contents of the pref list,
1021                    # must exit loop and try again
1022                    pref_list = pref_list[:i] + pref_list[i + 3:]
1023                    if pref_colors is not None:
1024                        pref_colors = pref_colors[:i//3] + pref_colors[i//3 + 1:]
1025                    break
1026
1027    return (prefs_changed, pref_list, pref_colors)
1028