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