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### USA
21
22__author__ = 'Damon Lynch'
23__copyright__ = "Copyright 2007-2020, Damon Lynch"
24
25import re
26from datetime import datetime, timedelta
27import string
28from collections import namedtuple
29import logging
30from typing import Sequence, Optional, List, Union
31import locale
32try:
33    # Use the default locale as defined by the LANG variable
34    locale.setlocale(locale.LC_ALL, '')
35except locale.Error:
36    pass
37
38
39
40from raphodo.preferences import DownloadsTodayTracker
41from raphodo.problemnotification import (
42    RenamingProblems, FilenameNotFullyGeneratedProblem, make_href,
43    FolderNotFullyGeneratedProblemProblem, Problem
44)
45from raphodo.rpdfile import RPDFile, Photo, Video
46from raphodo.storage import get_uri
47from raphodo.utilities import letters
48
49from raphodo.generatenameconfig import *
50
51
52MatchedSequences = namedtuple(
53    'MatchedSequences', 'session_sequence_no, sequence_letter, downloads_today, stored_sequence_no'
54)
55
56
57def convert_date_for_strftime(datetime_user_choice):
58    try:
59        return DATE_TIME_CONVERT[LIST_DATE_TIME_L2.index(datetime_user_choice)]
60    except KeyError:
61        raise PrefValueInvalidError(datetime_user_choice)
62
63
64class abstract_attribute():
65    """
66    http://stackoverflow.com/questions/32536176/how-to-define-lazy-variable-in-python-which-will-
67    raise-notimplementederror-for-a/32536493
68    """
69
70    def __get__(self, obj, type):
71        # Now we will iterate over the names on the class,
72        # and all its superclasses, and try to find the attribute
73        # name for this descriptor
74        # traverse the parents in the method resolution order
75        for cls in type.__mro__:
76            # for each cls thus, see what attributes they set
77            for name, value in cls.__dict__.items():
78                # we found ourselves here
79                if value is self:
80                    # if the property gets accessed as Child.variable,
81                    # obj will be done. For this case
82                    # If accessed as a_child.variable, the class Child is
83                    # in the type, and a_child in the obj.
84                    this_obj = obj if obj else type
85
86                    raise NotImplementedError(
87                         "%r does not have the attribute %r "
88                         "(abstract from class %r)" %
89                             (this_obj, name, cls.__name__))
90
91        # we did not find a match, should be rare, but prepare for it
92        raise NotImplementedError(
93            "%s does not set the abstract attribute <unknown>", type.__name__)
94
95
96GenerationErrors = Union[FilenameNotFullyGeneratedProblem, FolderNotFullyGeneratedProblemProblem]
97
98
99class NameGeneration:
100    """
101    Generate the name of a photo. Used as a base class for generating names
102    of videos, as well as subfolder names for both file types
103    """
104
105    def __init__(self,
106                 pref_list: List[str],
107                 problems: Optional[RenamingProblems]=None) -> None:
108        self.pref_list = pref_list
109        self.no_metadata = False
110
111        self.problems = problems
112        self.problem = abstract_attribute()  # type: GenerationErrors
113
114        self.strip_forward_slash = abstract_attribute()
115        self.add_extension = abstract_attribute()
116        self.L1_date_check = abstract_attribute()
117
118        self.L0 = ''
119        self.L1 = ''
120        self.L2 = ''
121
122    def _get_values_from_pref_list(self):
123        for i in range(0, len(self.pref_list), 3):
124            yield (self.pref_list[i], self.pref_list[i + 1], self.pref_list[i + 2])
125
126    def _get_date_component(self) -> str:
127        """
128        Returns portion of new file / subfolder name based on date time.
129        If the date is missing, will attempt to use the fallback date.
130        """
131
132        # step 1: get the correct value from metadata
133        if self.L1 == self.L1_date_check:
134            if self.no_metadata:
135                if self.L2 == SUBSECONDS:
136                    d = datetime.fromtimestamp(self.rpd_file.modification_time)
137                    if not d.microsecond:
138                        d = '00'
139                    try:
140                        d = str(round(int(str(d.microsecond)[:3]) / 10))
141                    except:
142                        d = '00'
143                    return d
144                d = datetime.fromtimestamp(self.rpd_file.ctime)
145            else:
146                if self.L2 == SUBSECONDS:
147                    d = self.rpd_file.metadata.sub_seconds(missing=None)
148                    if d is None:
149                        self.problem.missing_metadata.append(_(self.L2))
150                        return ''
151                    else:
152                        return d
153                else:
154                    d = self.rpd_file.date_time(missing=None)
155
156        elif self.L1 == TODAY:
157            d = datetime.now()
158        elif self.L1 == YESTERDAY:
159            delta = timedelta(days=1)
160            d = datetime.now() - delta
161        elif self.L1 == DOWNLOAD_TIME:
162            d = self.rpd_file.download_start_time
163        else:
164            raise TypeError("Date options invalid")
165
166        # step 2: if have a value, try to convert it to string format
167        if d:
168            try:
169                return d.strftime(convert_date_for_strftime(self.L2))
170            except Exception as e:
171                logging.warning(
172                    "Problem converting date/time value for file %s", self.rpd_file.full_file_name
173                )
174                self.problem.bad_converstion_date_time = True
175                self.problem.bad_conversion_exception = e
176
177        # step 3: handle a missing value using file modification time
178        if self.rpd_file.modification_time:
179            try:
180                d = datetime.fromtimestamp(self.rpd_file.modification_time)
181            except Exception:
182                logging.error(
183                    "Both file modification time and metadata date & time are invalid for file %s",
184                    self.rpd_file.full_file_name
185                )
186                self.problem.invalid_date_time = True
187                return ''
188        else:
189            self.problem.missing_metadata.append(_(self.L1))
190            return ''
191
192        try:
193            return d.strftime(convert_date_for_strftime(self.L2))
194        except:
195            logging.error(
196                "Both file modification time and metadata date & time are invalid for file %s",
197                self.rpd_file.full_file_name
198            )
199            self.problem.invalid_date_time = True
200            return ''
201
202    def _get_associated_file_extension(self, associate_file):
203        """
204        Generates extensions with correct capitalization for files like
205        thumbnail or audio files.
206        """
207
208        if not associate_file:
209            return None
210
211        extension = os.path.splitext(associate_file)[1]
212        if self.rpd_file.generate_extension_case == UPPERCASE:
213            extension = extension.upper()
214        elif self.rpd_file.generate_extension_case == LOWERCASE:
215            extension = extension.lower()
216        # else keep extension case the same as what it originally was
217        return extension
218
219    def _get_thm_extension(self) -> None:
220        """
221        Generates THM extension with correct capitalization, if needed
222        """
223        self.rpd_file.thm_extension = self._get_associated_file_extension(
224            self.rpd_file.thm_full_name
225        )
226
227    def _get_audio_extension(self) -> None:
228        """
229        Generates audio extension with correct capitalization, if needed
230        e.g. WAV or wav
231        """
232        self.rpd_file.audio_extension = self._get_associated_file_extension(
233            self.rpd_file.audio_file_full_name
234        )
235
236    def _get_xmp_extension(self) -> None:
237        """
238        Generates XMP extension with correct capitalization, if needed.
239        """
240
241        self.rpd_file.xmp_extension = self._get_associated_file_extension(
242            self.rpd_file.xmp_file_full_name
243        )
244
245    def _get_log_extension(self) -> None:
246        """
247        Generates LOG extension with correct capitalization, if needed.
248        """
249
250        self.rpd_file.log_extension = self._get_associated_file_extension(
251            self.rpd_file.log_file_full_name
252        )
253
254    def _get_filename_component(self):
255        """
256        Returns portion of new file / subfolder name based on the file name
257        """
258
259        name, extension = os.path.splitext(self.rpd_file.name)
260
261        if self.L1 == NAME:
262            filename = name
263        elif self.L1 == EXTENSION:
264            # Used in subfolder name generation
265            if extension:
266                # having the period when this is used as a part of a
267                # subfolder name
268                # is a bad idea when it is at the start!
269                filename = extension[1:]
270            else:
271                self.problem.missing_extension = True
272                return ""
273        elif self.L1 == IMAGE_NUMBER or self.L1 == VIDEO_NUMBER:
274            n = re.search("(?P<image_number>[0-9]+$)", name)
275            if not n:
276                self.problem.missing_image_no = True
277                return ''
278            else:
279                image_number = n.group("image_number")
280
281                if self.L2 == IMAGE_NUMBER_ALL:
282                    filename = image_number
283                elif self.L2 == IMAGE_NUMBER_1:
284                    filename = image_number[-1]
285                elif self.L2 == IMAGE_NUMBER_2:
286                    filename = image_number[-2:]
287                elif self.L2 == IMAGE_NUMBER_3:
288                    filename = image_number[-3:]
289                else:
290                    assert  self.L2 == IMAGE_NUMBER_4
291                    filename = image_number[-4:]
292        else:
293            raise TypeError("Incorrect filename option")
294
295        if self.L2 == UPPERCASE:
296            filename = filename.upper()
297        elif self.L2 == LOWERCASE:
298            filename = filename.lower()
299
300        return filename
301
302    def _get_metadata_component(self):
303        """
304        Returns portion of new image / subfolder name based on the metadata
305
306        Note: date time metadata found in _getDateComponent()
307        """
308
309        if self.L1 == APERTURE:
310            v = self.rpd_file.metadata.aperture()
311        elif self.L1 == ISO:
312            v = self.rpd_file.metadata.iso()
313        elif self.L1 == EXPOSURE_TIME:
314            v = self.rpd_file.metadata.exposure_time(alternativeFormat=True)
315        elif self.L1 == FOCAL_LENGTH:
316            v = self.rpd_file.metadata.focal_length()
317        elif self.L1 == CAMERA_MAKE:
318            v = self.rpd_file.metadata.camera_make()
319        elif self.L1 == CAMERA_MODEL:
320            v = self.rpd_file.metadata.camera_model()
321        elif self.L1 == SHORT_CAMERA_MODEL:
322            v = self.rpd_file.metadata.short_camera_model()
323        elif self.L1 == SHORT_CAMERA_MODEL_HYPHEN:
324            v = self.rpd_file.metadata.short_camera_model(includeCharacters="\-")
325        elif self.L1 == SERIAL_NUMBER:
326            v = self.rpd_file.metadata.camera_serial()
327        elif self.L1 == SHUTTER_COUNT:
328            v = self.rpd_file.metadata.shutter_count()
329            if v:
330                v = int(v)
331                padding = LIST_SHUTTER_COUNT_L2.index(self.L2) + 3
332                formatter = '%0' + str(padding) + "i"
333                v = formatter % v
334        elif self.L1 == FILE_NUMBER:
335            v = self.rpd_file.metadata.file_number()
336            if v and self.L2 == FILE_NUMBER_FOLDER:
337                v = v[:3]
338        elif self.L1 == OWNER_NAME:
339            v = self.rpd_file.metadata.owner_name()
340        elif self.L1 == ARTIST:
341            v = self.rpd_file.metadata.artist()
342        elif self.L1 == COPYRIGHT:
343            v = self.rpd_file.metadata.copyright()
344        else:
345            raise TypeError("Invalid metadata option specified")
346        if self.L1 in (CAMERA_MAKE, CAMERA_MODEL, SHORT_CAMERA_MODEL, SHORT_CAMERA_MODEL_HYPHEN,
347                        OWNER_NAME, ARTIST, COPYRIGHT):
348            if self.L2 == UPPERCASE:
349                v = v.upper()
350            elif self.L2 == LOWERCASE:
351                v = v.lower()
352        if not v:
353            self.problem.missing_metadata.append(_(self.L1))
354        return v
355
356    def _calculate_letter_sequence(self, sequence):
357
358        v = letters(sequence)
359        if self.L2 == UPPERCASE:
360            v = v.upper()
361
362        return v
363
364    def _format_sequence_no(self, value, amountToPad):
365        padding = LIST_SEQUENCE_NUMBERS_L2.index(amountToPad) + 1
366        formatter = '%0' + str(padding) + "i"
367        return formatter % value
368
369    def _get_downloads_today(self):
370        return self._format_sequence_no(
371            self.rpd_file.sequences.downloads_today, self.L2
372        )
373
374    def _get_session_sequence_no(self):
375        return self._format_sequence_no(
376            self.rpd_file.sequences.session_sequence_no, self.L2
377        )
378
379    def _get_stored_sequence_no(self):
380        return self._format_sequence_no(
381            self.rpd_file.sequences.stored_sequence_no, self.L2
382        )
383
384    def _get_sequence_letter(self):
385        return self._calculate_letter_sequence(
386            self.rpd_file.sequences.sequence_letter
387        )
388
389    def _get_sequences_component(self):
390        if self.L1 == DOWNLOAD_SEQ_NUMBER:
391            return self._get_downloads_today()
392        elif self.L1 == SESSION_SEQ_NUMBER:
393            return self._get_session_sequence_no()
394        elif self.L1 == STORED_SEQ_NUMBER:
395            return self._get_stored_sequence_no()
396        elif self.L1 == SEQUENCE_LETTER:
397            return self._get_sequence_letter()
398
399    def _get_component(self):
400        try:
401            if self.L0 == DATE_TIME:
402                return self._get_date_component()
403            elif self.L0 == TEXT:
404                return self.L1
405            elif self.L0 == FILENAME:
406                return self._get_filename_component()
407            elif self.L0 == METADATA:
408                return self._get_metadata_component()
409            elif self.L0 == SEQUENCES:
410                return self._get_sequences_component()
411            elif self.L0 == JOB_CODE:
412                return self.rpd_file.job_code
413            elif self.L0 == SEPARATOR:
414                return os.sep
415        except Exception as e:
416            self.problem.component_problem = _(self.L0)
417            self.problem.component_exception = e
418            return ''
419
420    def filter_strip_characters(self, name: str) -> str:
421        """
422        Filter out unwanted chacters from file and subfolder names
423        :param name: full name or name component
424        :return: filtered name
425        """
426
427        # remove any null characters - they are bad news in file names
428        name = name.replace('\x00', '')
429
430        # the user could potentially copy and paste a block of text with a carriage / line return
431        name = name.replace('\n', '')
432
433        if self.rpd_file.strip_characters:
434            for c in r'\:*?"<>|':
435                name = name.replace(c, '')
436
437        if self.strip_forward_slash:
438            name = name.replace('/', '')
439        return name
440
441    def _destination(self, rpd_file: RPDFile, name: str) -> str:
442        # implement in subclass
443        return ''
444
445    def _filter_name(self, name: str, parts: bool) -> str:
446        # implement in subclass if need be
447        return name
448
449    def generate_name(self, rpd_file: RPDFile,
450                      parts: Optional[bool]=False) -> Union[str, List[str]]:
451        """
452        Generate subfolder name(s), and photo/video filenames
453
454        :param rpd_file: rpd file for the name to generate
455        :param parts: if True, return string components in a list
456        :return: complete string or list of name components
457        """
458
459        self.rpd_file = rpd_file
460
461        if parts:
462            name = []
463        else:
464            name = ''
465
466        for self.L0, self.L1, self.L2 in self._get_values_from_pref_list():
467            v = self._get_component()
468            if parts:
469                name.append(self.filter_strip_characters(v))
470            elif v:
471                name += v
472
473        if not parts:
474            name = self.filter_strip_characters(name)
475            # strip any white space from the beginning and end of the name
476            name = name.strip()
477        elif name:
478            # likewise, strip any white space from the beginning and end of the name
479            name[0] = name[0].lstrip()
480            name[-1] = name[-1].rstrip()
481
482        if self.add_extension:
483            case = rpd_file.generate_extension_case
484            extension = os.path.splitext(rpd_file.name)[1]
485            if case == UPPERCASE:
486                extension = extension.upper()
487            elif case == LOWERCASE:
488                extension = extension.lower()
489            if parts:
490                name.append(extension)
491            else:
492                name += extension
493
494            self._get_thm_extension()
495            self._get_audio_extension()
496            self._get_xmp_extension()
497            self._get_log_extension()
498
499        name = self._filter_name(name, parts)
500
501        if self.problem.has_error():
502
503            rpd_file.name_generation_problem = True
504
505            if self.problems is not None:
506                self.problem.destination = self._destination(rpd_file=rpd_file, name=name)
507                self.problem.file_type = rpd_file.title
508                self.problem.source = rpd_file.get_souce_href()
509                self.problems.append(self.problem)
510
511        return name
512
513
514class PhotoName(NameGeneration):
515    """
516    Generate filenames for photos
517    """
518    def __init__(self, pref_list: List[str],
519                 problems: Optional[RenamingProblems]=None) -> None:
520        super().__init__(pref_list, problems)
521
522        self.problem = FilenameNotFullyGeneratedProblem()
523
524        self.strip_forward_slash = True
525        self.add_extension = True
526        self.L1_date_check = IMAGE_DATE  # used in _get_date_component()
527
528    def _destination(self, rpd_file: RPDFile, name: str) -> str:
529        if rpd_file.download_subfolder:
530            return make_href(
531                name=name,
532                uri=get_uri(
533                    full_file_name=os.path.join(
534                        rpd_file.download_folder, rpd_file.download_subfolder, name
535                    )
536                )
537            )
538        else:
539            return name
540
541
542class VideoName(PhotoName):
543    """
544    Generate filenames for videos
545    """
546    def __init__(self, pref_list: List[str],
547                 problems: Optional[RenamingProblems]=None) -> None:
548
549        super().__init__(pref_list, problems)
550
551        self.L1_date_check = VIDEO_DATE  # used in _get_date_component()
552
553    def _get_metadata_component(self):
554        """
555        Returns portion of video / subfolder name based on the metadata
556
557        Note: date time metadata found in _getDateComponent()
558        """
559        return get_video_metadata_component(self)
560
561
562class PhotoSubfolder(NameGeneration):
563    """
564    Generate subfolder names for photo files
565    """
566
567    def __init__(self, pref_list: List[str],
568                 problems: Optional[RenamingProblems]=None,
569                 no_metadata: Optional[bool]=False) -> None:
570        """
571        :param pref_list: subfolder generation preferences list
572        :param no_metadata: if True, halt as soon as the need for metadata
573        or a job code or sequence number becomes necessary
574        """
575
576        super().__init__(pref_list, problems)
577
578        if no_metadata:
579            self.pref_list = truncate_before_unwanted_subfolder_component(pref_list)
580        else:
581            self.pref_list = pref_list
582
583        self.no_metadata = no_metadata
584
585        self.problem = FolderNotFullyGeneratedProblemProblem()
586
587        self.strip_extraneous_white_space = re.compile(r'\s*%s\s*' % os.sep)
588        self.strip_forward_slash = False
589        self.add_extension = False
590        self.L1_date_check = IMAGE_DATE  # used in _get_date_component()
591
592    def _filter_name(self, name: str, parts: bool) -> str:
593        if not parts:
594            return self.filter_subfolder_characters(name)
595        return name
596
597    def _destination(self, rpd_file: RPDFile, name: str) -> str:
598        return make_href(
599                    name=name,
600                    uri = get_uri(path=os.path.join(rpd_file.download_folder, name))
601                )
602
603    def filter_subfolder_characters(self, subfolders: str) -> str:
604        """
605        Remove unwanted characters specific to the generation of subfolders
606        :param subfolders: the complete string containing the subfolders
607         (not component parts)
608        :return: filtered string
609        """
610
611        # subfolder value must never start with a separator, or else any
612        # os.path.join function call will fail to join a subfolder to its
613        # parent folder
614        if subfolders:
615            if subfolders[0] == os.sep:
616                subfolders = subfolders[1:]
617
618        # remove any spaces before and after a directory name
619        if subfolders and self.rpd_file.strip_characters:
620            subfolders = self.strip_extraneous_white_space.sub(os.sep, subfolders)
621
622        # remove any repeated directory separators
623        double_sep = os.sep * 2
624        subfolders = subfolders.replace(double_sep, os.sep)
625
626        # remove any trailing directory separators
627        while subfolders.endswith(os.sep):
628            subfolders = subfolders[:-1]
629
630        return subfolders
631
632
633class VideoSubfolder(PhotoSubfolder):
634    """
635    Generate subfolder names for video files
636    """
637
638    def __init__(self, pref_list: List[str],
639                 problems: Optional[RenamingProblems] = None,
640                 no_metadata: bool=False) -> None:
641        """
642        :param pref_list: subfolder generation preferences list
643        :param no_metadata: if True, halt as soon as the need for metadata
644        or a job code or sequence number becomes necessary
645        """
646        super().__init__(pref_list, problems, no_metadata)
647        self.L1_date_check = VIDEO_DATE  # used in _get_date_component()
648
649
650    def _get_metadata_component(self):
651        """
652        Returns portion of video / subfolder name based on the metadata
653
654        Note: date time metadata found in _getDateComponent()
655        """
656        return get_video_metadata_component(self)
657
658
659def truncate_before_unwanted_subfolder_component(pref_list: List[str]) -> List[str]:
660    r"""
661    truncate the preferences list to remove any subfolder element that
662    contains a metadata or a job code or sequence number
663
664    :param pref_list: subfolder prefs list
665    :return: truncated list
666
667    >>> print(truncate_before_unwanted_subfolder_component(PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV[0]))
668    ['Date time', 'Image date', 'YYYY', '/', '', '', 'Date time', 'Image date', 'YYYYMMDD']
669    >>> print(truncate_before_unwanted_subfolder_component(PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV[1]))
670    ['Date time', 'Image date', 'YYYY', '/', '', '', 'Date time', 'Image date', 'YYYY-MM-DD']
671    >>> print(truncate_before_unwanted_subfolder_component(PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV[2]))
672    ['Date time', 'Image date', 'YYYY', '/', '', '', 'Date time', 'Image date', 'YYYY_MM_DD']
673    >>> print(truncate_before_unwanted_subfolder_component(PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV[3]))
674    ['Date time', 'Image date', 'YYYY']
675    >>> print(truncate_before_unwanted_subfolder_component(PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV[4]))
676    ... # doctest: +NORMALIZE_WHITESPACE
677    ['Date time', 'Image date', 'YYYY', '/', '', '', 'Date time', 'Image date', 'YYYY',
678    'Date time', 'Image date', 'MM']
679    >>> print(truncate_before_unwanted_subfolder_component([JOB_CODE, '', '',]))
680    []
681    >>> pl = [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[11]]
682    >>> print(truncate_before_unwanted_subfolder_component(pl))
683    ['Date time', 'Image date', 'YYYY']
684    """
685
686    rl = [pref_list[i] for i in range(0, len(pref_list), 3)]
687    truncate = -1
688    for idx, value in enumerate(rl):
689        if value in (METADATA, SEQUENCES, JOB_CODE):
690            break
691        if idx == len(rl) - 1:
692            truncate = idx + 1
693        elif value == SEPARATOR:
694            truncate = idx
695
696    if truncate >= 0:
697        return pref_list[:truncate * 3]
698    return []
699
700
701def get_video_metadata_component(video: Union[VideoSubfolder, VideoName]):
702    """
703    Returns portion of video / subfolder name based on the metadata
704
705    This is outside of a class definition because of the inheritance
706    hierarchy.
707    """
708
709    if video.L1 == CODEC:
710        v = video.rpd_file.metadata.codec()
711    elif video.L1 == WIDTH:
712        v = video.rpd_file.metadata.width()
713    elif video.L1 == HEIGHT:
714        v = video.rpd_file.metadata.height()
715    elif video.L1 == FPS:
716        v = video.rpd_file.metadata.frames_per_second()
717    elif video.L1 == LENGTH:
718        v = video.rpd_file.metadata.length()
719    else:
720        raise TypeError("Invalid metadata option specified")
721    if video.L1 in [CODEC]:
722        if video.L2 == UPPERCASE:
723            v = v.upper()
724        elif video.L2 == LOWERCASE:
725            v = v.lower()
726    if not v:
727        video.problem.missing_metadata.append(_(video.L1))
728    return v
729
730
731class Sequences:
732    """
733    Stores sequence numbers and letters used in generating file names.
734    """
735
736    def __init__(self, downloads_today_tracker: DownloadsTodayTracker,
737                 stored_sequence_no: int) -> None:
738        self._session_sequence_no = 0
739        self._sequence_letter = -1
740        self.downloads_today_tracker = downloads_today_tracker
741        self._stored_sequence_no = stored_sequence_no
742        self.matched_sequences = None
743        self.use_matched_sequences = False
744
745    @property
746    def session_sequence_no(self) -> int:
747        if self.use_matched_sequences:
748            return self.matched_sequences.session_sequence_no
749        else:
750            return self._session_sequence_no + 1
751
752    @property
753    def sequence_letter(self) -> int:
754        if self.use_matched_sequences:
755            return self.matched_sequences.sequence_letter
756        else:
757            return self._sequence_letter + 1
758
759    def increment(self, uses_session_sequence_no, uses_sequence_letter) -> None:
760        if uses_session_sequence_no:
761            self._session_sequence_no += 1
762        if uses_sequence_letter:
763            self._sequence_letter += 1
764
765    @property
766    def downloads_today(self) -> int:
767        if self.use_matched_sequences:
768            return self.matched_sequences.downloads_today
769        else:
770            return self._get_downloads_today()
771
772    def _get_downloads_today(self) -> int:
773        v = self.downloads_today_tracker.get_downloads_today()
774        if v == -1:
775            return 1
776        else:
777            return v + 1
778
779    @property
780    def stored_sequence_no(self) -> int:
781        if self.use_matched_sequences:
782            return self.matched_sequences.stored_sequence_no
783        else:
784            return self._stored_sequence_no + 1
785
786    @stored_sequence_no.setter
787    def stored_sequence_no(self, value: int) -> None:
788        self._stored_sequence_no = value
789
790    def create_matched_sequences(self) -> MatchedSequences:
791        return MatchedSequences(
792            session_sequence_no=self._session_sequence_no + 1,
793            sequence_letter=self._sequence_letter + 1,
794            downloads_today=self._get_downloads_today(),
795            stored_sequence_no=self._stored_sequence_no + 1
796        )
797