diff --git a/bin/ffx.py b/bin/ffx.py index 31b472c..6d85c66 100755 --- a/bin/ffx.py +++ b/bin/ffx.py @@ -440,10 +440,12 @@ def convert(ctx, if context['use_tmdb']: + click.echo(f"Querying TMDB for show_id={currentShowDescriptor.getId()} season={mediaFileProperties.getSeason()} episode{mediaFileProperties.getEpisode()}") tmdbEpisodeResult = tc.queryEpisode(currentShowDescriptor.getId(), mediaFileProperties.getSeason(), mediaFileProperties.getEpisode()) + click.echo(f"tmdbEpisodeResult={tmdbEpisodeResult}") if tmdbEpisodeResult: - sourceFileBasename = tc.getEpisodeFileBasename(currentShowDescriptor.getFilenamePrefix(), + sourceFileBasename = TmdbController.getEpisodeFileBasename(currentShowDescriptor.getFilenamePrefix(), tmdbEpisodeResult['name'], mediaFileProperties.getSeason(), mediaFileProperties.getEpisode(), diff --git a/bin/ffx/ffx_controller.py b/bin/ffx/ffx_controller.py index c5f290b..6434791 100644 --- a/bin/ffx/ffx_controller.py +++ b/bin/ffx/ffx_controller.py @@ -175,12 +175,12 @@ class FfxController(): # sourceTrackDescriptors = [] if self.__sourceMediaDescriptor is None else self.__sourceMediaDescriptor.getAllTrackDescriptors() targetTrackDescriptors = self.__targetMediaDescriptor.getAllTrackDescriptors() - dispositionTokens = [] for trackIndex in range(len(targetTrackDescriptors)): td = targetTrackDescriptors[trackIndex] + #sd = sourceTrackDescriptors[trackIndex] #HINT: No dispositions for pgs subtitle tracks that have no external file source if (td.getExternalSourceFilePath() diff --git a/bin/ffx/file_properties.py b/bin/ffx/file_properties.py index 7fabceb..21935e1 100644 --- a/bin/ffx/file_properties.py +++ b/bin/ffx/file_properties.py @@ -43,24 +43,35 @@ class FileProperties(): self.__pc = PatternController(context) + # db pattern boruto_[sS]([0-9]+)[eE]([0-9]+).mkv + + # Checking if database contains matching pattern matchResult = self.__pc.matchFilename(self.__sourceFilename) + + self.__logger.debug(f"FileProperties.__init__(): Match result {matchResult}") self.__pattern: Pattern = matchResult['pattern'] if matchResult else None - matchedGroups = matchResult['match'].groups() if matchResult else {} - seIndicator = matchedGroups[0] if matchedGroups else self.__sourceFilename - - se_match = re.search(FileProperties.SEASON_EPISODE_INDICATOR_MATCH, seIndicator) - e_match = re.search(FileProperties.EPISODE_INDICATOR_MATCH, seIndicator) - - self.__season = -1 - self.__episode = -1 - - if se_match is not None: - self.__season = int(se_match.group(1)) - self.__episode = int(se_match.group(2)) - elif e_match is not None: - self.__episode = int(e_match.group(1)) + if matchResult: + databaseMatchedGroups = matchResult['match'].groups() + self.__season = databaseMatchedGroups[0] + self.__episode = databaseMatchedGroups[1] + + else: + self.__logger.debug(f"FileProperties.__init__(): Checking file name for indicator {self.__sourceFilename}") + + se_match = re.search(FileProperties.SEASON_EPISODE_INDICATOR_MATCH, self.__sourceFilename) + e_match = re.search(FileProperties.EPISODE_INDICATOR_MATCH, self.__sourceFilename) + + if se_match is not None: + self.__season = int(se_match.group(1)) + self.__episode = int(se_match.group(2)) + elif e_match is not None: + self.__season = -1 + self.__episode = int(e_match.group(1)) + else: + self.__season = -1 + self.__episode = -1 def getFormatData(self): diff --git a/bin/ffx/media_controller.py b/bin/ffx/media_controller.py new file mode 100644 index 0000000..fd2bfd2 --- /dev/null +++ b/bin/ffx/media_controller.py @@ -0,0 +1,47 @@ +import click, re + +from ffx.model.pattern import Pattern +from ffx.media_descriptor import MediaDescriptor + +from ffx.tag_controller import TagController +from ffx.track_controller import TrackController + +class MediaController(): + + def __init__(self, context): + + self.context = context + self.Session = self.context['database']['session'] # convenience + + self.__logger = context['logger'] + + self.__tc = TrackController(context = context) + self.__tac = TagController(context = context) + + def setPatternMediaDescriptor(self, mediaDescriptor: MediaDescriptor, patternId: int): + + try: + + pid = int(patternId) + + s = self.Session() + q = s.query(Pattern).filter(Pattern.id == pid) + + if q.count(): + pattern = q.first + + for mediaTagKey, mediaTagValue in mediaDescriptor.getTags(): + self.__tac.updateMediaTag(pid, mediaTagKey, mediaTagValue) + for trackDescriptor in mediaDescriptor.getAllTrackDescriptors(): + self.__tc.addTrack(trackDescriptor, patternId = pid) + + s.commit() + return True + else: + return False + + except Exception as ex: + self.__logger.error(f"MediaController.setPatternMediaDescriptor(): {repr(ex)}") + raise click.ClickException(f"MediaController.setPatternMediaDescriptor(): {repr(ex)}") + finally: + s.close() diff --git a/bin/ffx/media_descriptor.py b/bin/ffx/media_descriptor.py index 4d1af78..ad0ae0d 100644 --- a/bin/ffx/media_descriptor.py +++ b/bin/ffx/media_descriptor.py @@ -501,3 +501,11 @@ class MediaDescriptor: # click.echo(f"Found matching subtitle file {msfd["path"]}\n") self.__logger.debug(f"importSubtitles(): Found matching subtitle file {msfd["path"]}") matchingSubtitleTrackDescriptor[0].setExternalSourceFilePath(msfd["path"]) + + + def getConfiguration(self, label: str = ''): + yield f"--- {label if label else 'MediaDescriptor '+str(id(self))} {' '.join([str(k)+'='+str(v) for k,v in self.__mediaTags.items()])}" + for td in self.getAllTrackDescriptors(): + yield (f"{td.getIndex()}:{td.getType().indicator()}:{td.getSubIndex()} " + + '|'.join([d.indicator() for d in td.getDispositionSet()]) + + ' ' + ' '.join([str(k)+'='+str(v) for k,v in td.getTags().items()])) diff --git a/bin/ffx/media_details_screen.py b/bin/ffx/media_details_screen.py index b2971b3..fc27760 100644 --- a/bin/ffx/media_details_screen.py +++ b/bin/ffx/media_details_screen.py @@ -504,14 +504,14 @@ class MediaDetailsScreen(Screen): if patternDescriptor: patternId = self.__pc.addPattern(patternDescriptor) + if patternId: + self.highlightPattern(False) - self.highlightPattern(False) + for tagKey, tagValue in self.__currentMediaDescriptor.getTags().items(): + self.__tac.updateMediaTag(patternId, tagKey, tagValue) - for tagKey, tagValue in self.__currentMediaDescriptor.getTags().items(): - self.__tac.updateMediaTag(patternId, tagKey, tagValue) - - for trackDescriptor in self.__currentMediaDescriptor.getAllTrackDescriptors(): - self.__tc.addTrack(trackDescriptor, patternId = patternId) + for trackDescriptor in self.__currentMediaDescriptor.getAllTrackDescriptors(): + self.__tc.addTrack(trackDescriptor, patternId = patternId) def action_new_pattern(self): diff --git a/bin/ffx/pattern_controller.py b/bin/ffx/pattern_controller.py index 47c19cb..f9e7740 100644 --- a/bin/ffx/pattern_controller.py +++ b/bin/ffx/pattern_controller.py @@ -25,7 +25,7 @@ class PatternController(): s.commit() return pattern.getId() else: - return None + return 0 except Exception as ex: raise click.ClickException(f"PatternController.addPattern(): {repr(ex)}") @@ -116,7 +116,8 @@ class PatternController(): s.close() - def matchFilename(self, filename : str) -> re.Match: + def matchFilename(self, filename : str) -> dict: + """Returns dict {'match': , 'pattern': } or empty dict of no pattern was found""" try: s = self.Session() @@ -126,7 +127,7 @@ class PatternController(): for pattern in q.all(): patternMatch = re.search(str(pattern.pattern), str(filename)) - if patternMatch: + if patternMatch is not None: matchResult['match'] = patternMatch matchResult['pattern'] = pattern diff --git a/bin/ffx/pattern_details_screen.py b/bin/ffx/pattern_details_screen.py index 0ea2406..01fa20c 100644 --- a/bin/ffx/pattern_details_screen.py +++ b/bin/ffx/pattern_details_screen.py @@ -345,8 +345,7 @@ class PatternDetailsScreen(Screen): else: patternId = self.__pc.addPattern(patternDescriptor) - if patternId is not None: - + if patternId: self.dismiss(patternDescriptor) else: #TODO: Meldung diff --git a/bin/ffx/show_descriptor.py b/bin/ffx/show_descriptor.py index 09e4702..566d142 100644 --- a/bin/ffx/show_descriptor.py +++ b/bin/ffx/show_descriptor.py @@ -55,33 +55,34 @@ class ShowDescriptor(): else: self.__showYear = -1 + if ShowDescriptor.INDEX_SEASON_DIGITS_KEY in kwargs.keys(): if type(kwargs[ShowDescriptor.INDEX_SEASON_DIGITS_KEY]) is not int: raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDEX_SEASON_DIGITS_KEY} is required to be of type int") self.__indexSeasonDigits = kwargs[ShowDescriptor.INDEX_SEASON_DIGITS_KEY] else: - self.__indexSeasonDigits = -1 + self.__indexSeasonDigits = ShowDescriptor.DEFAULT_INDEX_SEASON_DIGITS if ShowDescriptor.INDEX_EPISODE_DIGITS_KEY in kwargs.keys(): if type(kwargs[ShowDescriptor.INDEX_EPISODE_DIGITS_KEY]) is not int: raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDEX_EPISODE_DIGITS_KEY} is required to be of type int") self.__indexEpisodeDigits = kwargs[ShowDescriptor.INDEX_EPISODE_DIGITS_KEY] else: - self.__indexEpisodeDigits = -1 + self.__indexEpisodeDigits = ShowDescriptor.DEFAULT_INDEX_EPISODE_DIGITS if ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY in kwargs.keys(): if type(kwargs[ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY]) is not int: raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY} is required to be of type int") self.__indicatorSeasonDigits = kwargs[ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY] else: - self.__indicatorSeasonDigits = -1 + self.__indicatorSeasonDigits = ShowDescriptor.DEFAULT_INDICATOR_SEASON_DIGITS if ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY in kwargs.keys(): if type(kwargs[ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY]) is not int: raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY} is required to be of type int") self.__indicatorEpisodeDigits = kwargs[ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY] else: - self.__indicatorEpisodeDigits = -1 + self.__indicatorEpisodeDigits = ShowDescriptor.DEFAULT_INDICATOR_EPISODE_DIGITS def getId(self): diff --git a/bin/ffx/test/indicator_combinator.py b/bin/ffx/test/indicator_combinator.py index c2e41c2..6b8ba2d 100644 --- a/bin/ffx/test/indicator_combinator.py +++ b/bin/ffx/test/indicator_combinator.py @@ -17,12 +17,16 @@ class IndicatorCombinator(): if season == -1 and episode == -1: return { 'variant': 'S00E00', - 'indicator': '' + 'indicator': '', + 'season': season, + 'episode': episode } else: return { - 'variant': f"S{season+1:02d}E{episode+1:02d}", - 'indicator': f"S{season+1:02d}E{episode+1:02d}" + 'variant': f"S{season:02d}E{episode:02d}", + 'indicator': f"S{season:02d}E{episode:02d}", + 'season': season, + 'episode': episode } def assertFunc(self, testObj = {}): @@ -36,4 +40,4 @@ class IndicatorCombinator(): yield self.getPayload() for season in range(IndicatorCombinator.MAX_SEASON): for episode in range(IndicatorCombinator.MAX_EPISODE): - yield self.getPayload(season, episode) + yield self.getPayload(season+1, episode+1) diff --git a/bin/ffx/test/scenario.py b/bin/ffx/test/scenario.py index c23d0ef..5834e93 100644 --- a/bin/ffx/test/scenario.py +++ b/bin/ffx/test/scenario.py @@ -2,7 +2,6 @@ import os, glob, sys, importlib, glob, inspect from ffx.test.helper import createEmptyDirectory - class Scenario(): """Scenarios diff --git a/bin/ffx/test/scenario_2.py b/bin/ffx/test/scenario_2.py index 62c7112..40e5c74 100644 --- a/bin/ffx/test/scenario_2.py +++ b/bin/ffx/test/scenario_2.py @@ -91,8 +91,13 @@ class Scenario2(Scenario): if rc: self._logger.debug(f"{variantLabel}: Process returned ERROR {rc} ({err})") + # Phase 4: Evaluate results + resultFilenames = [rf for rf in self.getFilenamesInTestDirectory() if rf != 'ffmpeg2pass-0.log' and rf != variantFilename] + + self._logger.debug(f"{variantLabel}: Result filenames: {resultFilenames}") + try: jobFailed = bool(rc) diff --git a/bin/ffx/test/scenario_4.py b/bin/ffx/test/scenario_4.py index 5962ac8..3d8b777 100644 --- a/bin/ffx/test/scenario_4.py +++ b/bin/ffx/test/scenario_4.py @@ -4,6 +4,8 @@ from .scenario import Scenario from ffx.test.helper import createMediaTestFile from ffx.process import executeProcess +from ffx.database import databaseContext +from ffx.test.helper import createEmptyDirectory from ffx.file_properties import FileProperties @@ -14,18 +16,98 @@ from ffx.track_type import TrackType from ffx.track_disposition import TrackDisposition from ffx.test.media_combinator import MediaCombinator +from ffx.test.indicator_combinator import IndicatorCombinator +from ffx.show_descriptor import ShowDescriptor +from ffx.show_controller import ShowController +from ffx.pattern_controller import PatternController +from ffx.media_controller import MediaController +from ffx.tmdb_controller import TmdbController +from ffx.tmdb_controller import TMDB_API_KEY_NOT_PRESENT_EXCEPTION class Scenario4(Scenario): + TEST_SHOW_IDENTIFIER = 83095 + TEST_SHOW_NAME = 'The Rising of the Shield Hero' + TEST_SHOW_YEAR = 2019 + + TEST_FILE_LABEL = 'rotsh' + TEST_FILE_EXTENSION = 'mkv' + + TEST_PATTERN = f"{TEST_FILE_LABEL}_{FileProperties.SEASON_EPISODE_INDICATOR_MATCH}.{TEST_FILE_EXTENSION}" + + EXPECTED_FILE_EXTENSION = 'webm' + + def __init__(self, context): super().__init__(context) + self.__tmdbApiKey = os.environ.get('TMDB_API_KEY', None) + if self.__tmdbApiKey is None: + raise TMDB_API_KEY_NOT_PRESENT_EXCEPTION + + self.__testDbFilePath = os.path.join(self._testDirectory, 'test.db') + self.createEmptyTestDatabase() + + self.__ic = IndicatorCombinator(context = context) + + self.__sc = ShowController(context = context) + self.__pc = PatternController(context = context) + self.__mc = MediaController(context = context) + + self.__tc = TmdbController() + + def getScenario(self): return self.__class__.__name__[8:] + def createEmptyTestDatabase(self): + + if not self._context['database'] is None: + self._context['database']['engine'].dispose() + + if os.path.isfile(self.__testDbFilePath): + os.unlink(self.__testDbFilePath) + self._context['database'] = None + + self._logger.debug(f"Creating test db with path {self.__testDbFilePath}") + self._context['database'] = databaseContext(databasePath=self.__testDbFilePath) + + def prepareTestDatabase(self, sourceMediaDescriptor: MediaDescriptor): + + if not self._context['database'] is None: + self._context['database']['engine'].dispose() + + if os.path.isfile(self.__testDbFilePath): + os.unlink(self.__testDbFilePath) + self._context['database'] = None + + self._logger.debug(f"Creating test db with path {self.__testDbFilePath}") + self._context['database'] = databaseContext(databasePath=self.__testDbFilePath) + + kwargs = {} + + kwargs[ShowDescriptor.ID_KEY] = Scenario4.TEST_SHOW_IDENTIFIER + kwargs[ShowDescriptor.NAME_KEY] = Scenario4.TEST_SHOW_NAME + kwargs[ShowDescriptor.YEAR_KEY] = Scenario4.TEST_SHOW_YEAR + + self.__testShowDescriptor = ShowDescriptor(**kwargs) + + self._logger.debug(f"Adding test show '{self.__testShowDescriptor.getFilenamePrefix()}' to test db") + if not self.__sc.updateShow(self.__testShowDescriptor): + raise click.ClickException('Could not create test show in db') + + testPatternDescriptor = { + 'show_id': Scenario4.TEST_SHOW_IDENTIFIER, + 'pattern': Scenario4.TEST_PATTERN + } + patternId = self.__pc.addPattern(testPatternDescriptor) + + if patternId: + self.__mc.setPatternMediaDescriptor(sourceMediaDescriptor, patternId) + def job(self, yieldObj: dict): @@ -44,11 +126,11 @@ class Scenario4(Scenario): variantLabel = f"{self.__class__.__name__} Variant {variantIdentifier}" sourceMediaDescriptor: MediaDescriptor = targetYieldObj['payload'] - #presetMediaDescriptor: MediaDescriptor = targetYieldObj['payload']['preset'] + presetMediaDescriptor: MediaDescriptor = presetYieldObj['payload'] - assertSelectorList: list = targetYieldObj['assertSelectors'] - assertFuncList = targetYieldObj['assertFuncs'] - shouldFail = targetYieldObj['shouldFail'] + assertSelectorList: list = presetYieldObj['assertSelectors'] + assertFuncList = presetYieldObj['assertFuncs'] + shouldFail = presetYieldObj['shouldFail'] try: jellyfinSelectorIndex = assertSelectorList.index('J') @@ -61,63 +143,113 @@ class Scenario4(Scenario): if self._context['test_variant'] and variantIdentifier != self._context['test_variant']: return + for l in sourceMediaDescriptor.getConfiguration(label = 'sourceMediaDescriptor'): + self._logger.debug(l) + + for l in presetMediaDescriptor.getConfiguration(label = 'presetMediaDescriptor'): + self._logger.debug(l) self._logger.debug(f"Running Job: {variantLabel}") # Phase 1: Setup source files self.clearTestDirectory() - mediaFilePath = createMediaTestFile(mediaDescriptor=sourceMediaDescriptor, directory=self._testDirectory, logger=self._logger, length = 2) + self.createEmptyTestDatabase() + self.prepareTestDatabase(sourceMediaDescriptor) + + testFileList = [] + for indicatorObj in [y for y in self.__ic.getYield() if y['indicator']]: + + indicator = indicatorObj['indicator'] + + testFileObj = {} + testFileObj['season'] = indicatorObj['season'] + testFileObj['episode'] = indicatorObj['episode'] + + testFileObj['basename'] = f"{Scenario4.TEST_FILE_LABEL}_{indicator}" + + testFileObj['path'] = createMediaTestFile(mediaDescriptor = presetMediaDescriptor, + directory = self._testDirectory, + baseName = testFileObj['basename'], + logger=self._logger, + length = 2) + testFileObj['filename'] = f"{testFileObj['basename']}.{Scenario4.TEST_FILE_EXTENSION}" + + testFileList.append(testFileObj) + + # Phase 2: Prepare database -# # Phase 2: Prepare database -# # Phase 3: Run ffx commandSequence = [sys.executable, self._ffxExecutablePath, - 'convert', - mediaFilePath, - '--no-prompt'] + '--database-file', + self.__testDbFilePath, + 'convert'] + commandSequence += [tfo['filename'] for tfo in testFileList] + + commandSequence += ['--no-prompt'] - if not testContext['use_jellyfin']: - commandSequence += ['--no-jellyfin'] + # if not testContext['use_jellyfin']: + # commandSequence += ['--no-jellyfin'] self._logger.debug(f"{variantLabel}: Test sequence: {commandSequence}") out, err, rc = executeProcess(commandSequence, directory = self._testDirectory) - if out: - self._logger.debug(f"{variantLabel}: Process output: {out}") + # if out: + # self._logger.debug(f"{variantLabel}: Process output: {out}") if rc: self._logger.debug(f"{variantLabel}: Process returned ERROR {rc} ({err})") + # Phase 4: Evaluate results + resultFilenames = [rf for rf in self.getFilenamesInTestDirectory() if rf.endswith(f".{Scenario4.EXPECTED_FILE_EXTENSION}")] + + self._logger.debug(f"{variantLabel}: Result filenames: {resultFilenames}") + + try: - jobFailed = bool(rc) + assert not (bool(rc) + ), f"Process failed" + + for tfo in testFileList: - self._logger.debug(f"{variantLabel}: Should fail: {shouldFail} / actually failed: {jobFailed}") + tmdbEpisodeResult = self.__tc.queryEpisode(Scenario4.TEST_SHOW_IDENTIFIER, + tfo['season'], tfo['episode']) - assert (jobFailed == shouldFail - ), f"Process {'failed' if jobFailed else 'did not fail'}" + expectedFileBasename = TmdbController.getEpisodeFileBasename(self.__testShowDescriptor.getFilenamePrefix(), + tmdbEpisodeResult['name'], + tfo['season'], tfo['episode']) - if not jobFailed: + expectedFilename = f"{expectedFileBasename}.{Scenario4.EXPECTED_FILE_EXTENSION}" + expectedFilePath = os.path.join(self._testDirectory, expectedFilename) - resultFile = os.path.join(self._testDirectory, 'media.webm') + assert (os.path.isfile(expectedFilePath) + ), f"Result file '{expectedFilename}' in path '{self._testDirectory}' wasn't created" - assert (os.path.isfile(resultFile) - ), f"Result file 'media.webm' in path '{self._testDirectory}' wasn't created" + ### + # - resultFileProperties = FileProperties(testContext, resultFile) - resultMediaDescriptor = resultFileProperties.getMediaDescriptor() + rfp = FileProperties(testContext, expectedFilePath) + self._logger.debug(f"{variantLabel}: Result file properties: {rfp.getFilename()} season={rfp.getSeason()} episode={rfp.getEpisode()}") + + rmd = rfp.getMediaDescriptor() + rmt = rmd.getAllTrackDescriptors() + + for l in rmd.getConfiguration(label = 'resultMediaDescriptor'): + self._logger.debug(l) if testContext['use_jellyfin']: sourceMediaDescriptor.applyJellyfinOrder() - resultMediaDescriptor.applySourceIndices(sourceMediaDescriptor) - resultMediaTracks = resultMediaDescriptor.getAllTrackDescriptors() + # num tracks differ + rmd.applySourceIndices(sourceMediaDescriptor) + + for assertIndex in range(len(assertSelectorList)): @@ -125,17 +257,17 @@ class Scenario4(Scenario): assertFunc = assertFuncList[assertIndex] assertVariant = variantList[assertIndex] - if assertSelector == 'M': - assertFunc() - for variantIndex in range(len(assertVariant)): - assert (assertVariant[variantIndex].lower() == resultMediaTracks[variantIndex].getType().indicator() - ), f"Stream #{variantIndex} is not of type {resultMediaTracks[variantIndex].getType().label()}" - - elif assertSelector == 'AD' or assertSelector == 'AT': - assertFunc({'tracks': resultMediaDescriptor.getAudioTracks()}) + # if assertSelector == 'M': + # assertFunc() + # for variantIndex in range(len(assertVariant)): + # assert (assertVariant[variantIndex].lower() == rmd.getType().indicator() + # ), f"Stream #{variantIndex} is not of type {rmd.getType().label()}" +# + if assertSelector == 'AD' or assertSelector == 'AT': + assertFunc({'tracks': rmd.getAudioTracks()}) elif assertSelector == 'SD' or assertSelector == 'ST': - assertFunc({'tracks': resultMediaDescriptor.getSubtitleTracks()}) + assertFunc({'tracks': rmd.getSubtitleTracks()}) elif type(assertSelector) is str: if assertSelector == 'J': @@ -148,9 +280,13 @@ class Scenario4(Scenario): self._reportLogger.error(f"{variantLabel}: Test FAILED ({ae})") + exit() def run(self): - MC_list = MediaCombinator.getAllClassReferences() + + + + MC_list = [MediaCombinator.getClassReference(6)] for MC in MC_list: self._logger.debug(f"MC={MC.__name__}") mc = MC(context = self._context, createPresets = True) diff --git a/bin/ffx/tmdb_controller.py b/bin/ffx/tmdb_controller.py index aec8293..365287f 100644 --- a/bin/ffx/tmdb_controller.py +++ b/bin/ffx/tmdb_controller.py @@ -1,19 +1,56 @@ -import os, click, requests, json +import os, click, requests, json, time, logging + +class TMDB_REQUEST_EXCEPTION(Exception): + def __init__(self, statusCode, statusMessage): + errorMessage = f"TMDB query failed with status code {statusCode}: {statusMessage}" + super().__init__(errorMessage) + +class TMDB_API_KEY_NOT_PRESENT_EXCEPTION(Exception): + def __str__(self): + return 'TMDB api key is not available, please set environment variable TMDB_API_KEY' + +class TMDB_EXCESSIVE_USAGE_EXCEPTION(Exception): + def __str__(self): + return 'Rate limit was triggered too often' class TmdbController(): DEFAULT_LANGUAGE = 'de-DE' - def __init__(self): - - try: - self.__tmdbApiKey = os.environ['TMDB_API_KEY'] - except KeyError: - raise click.ClickException('TMDB api key is not available, please set environment variable TMDB_API_KEY') + RATE_LIMIT_WAIT_SECONDS = 10 + RATE_LIMIT_RETRIES = 3 + def __init__(self, context = None): + self.__context = context + self.__logger = (context['logger'] if context is not None and 'logger' in context.keys() + else logging.getLogger('FFX').addHandler(logging.NullHandler())) + + self.__tmdbApiKey = os.environ.get('TMDB_API_KEY', None) + if self.__tmdbApiKey is None: + raise TMDB_API_KEY_NOT_PRESENT_EXCEPTION + self.tmdbLanguage = TmdbController.DEFAULT_LANGUAGE + + def getTmdbRequest(self, tmdbUrl): + retries = TmdbController.RATE_LIMIT_RETRIES + while True: + response = requests.get(tmdbUrl) + if response.status_code == 429: + if not retries: + raise TMDB_EXCESSIVE_USAGE_EXCEPTION() + self.__logger.warning('TMDB Rate limit (status_code 429)') + time.sleep(TmdbController.RATE_LIMIT_WAIT_SECONDS) + retries -= 1 + else: + jsonResult = response.json() + if ('success' in jsonResult.keys() + and not jsonResult['success']): + raise TMDB_REQUEST_EXCEPTION(jsonResult['status_code'], jsonResult['status_message']) + return jsonResult + + def queryShow(self, showId): """ First level keys in the response object: @@ -55,22 +92,8 @@ class TmdbController(): tmdbUrl = f"https://api.themoviedb.org/3/tv/{showId}{urlParams}" - #TODO Check for result - try: - #TODO: Content Type aware processing - # response = requests.get(tmdbUrl) - # response.encoding = 'utf-8' - # return response.json() - # response = requests.get(tmdbUrl) - - # contentType = response.headers.get('Content-Type') - # print(content_type) # Example: 'application/json; charset=UTF-8' - - # decoded_content = response.content.decode('utf-8') - # return json.loads(decoded_content) - return requests.get(tmdbUrl).json() - except: - return {} + return self.getTmdbRequest(tmdbUrl) + def queryEpisode(self, showId, season, episode): """ @@ -94,21 +117,18 @@ class TmdbController(): tmdbUrl = f"https://api.themoviedb.org/3/tv/{showId}/season/{season}/episode/{episode}{urlParams}" - #TODO Check for result - try: - return requests.get(tmdbUrl).json() - except: - return {} - - def getEpisodeFileBasename(self, - showName, - episodeName, - season, - episode, - indexSeasonDigits = 2, - indexEpisodeDigits = 2, - indicatorSeasonDigits = 2, - indicatorEpisodeDigits = 2): + return self.getTmdbRequest(tmdbUrl) + + + @staticmethod + def getEpisodeFileBasename(showName, + episodeName, + season, + episode, + indexSeasonDigits = 2, + indexEpisodeDigits = 2, + indicatorSeasonDigits = 2, + indicatorEpisodeDigits = 2): """ One Piece: indexSeasonDigits = 0, diff --git a/bin/ffx/track_disposition.py b/bin/ffx/track_disposition.py index 42ea02f..df447ca 100644 --- a/bin/ffx/track_disposition.py +++ b/bin/ffx/track_disposition.py @@ -5,25 +5,25 @@ from enum import Enum class TrackDisposition(Enum): - DEFAULT = {"name": "default", "index": 0} - FORCED = {"name": "forced", "index": 1} - - DUB = {"name": "dub", "index": 2} - ORIGINAL = {"name": "original", "index": 3} - COMMENT = {"name": "comment", "index": 4} - LYRICS = {"name": "lyrics", "index": 5} - KARAOKE = {"name": "karaoke", "index": 6} - HEARING_IMPAIRED = {"name": "hearing_impaired", "index": 7} - VISUAL_IMPAIRED = {"name": "visual_impaired", "index": 8} - CLEAN_EFFECTS = {"name": "clean_effects", "index": 9} - ATTACHED_PIC = {"name": "attached_pic", "index": 10} - TIMED_THUMBNAILS = {"name": "timed_thumbnails", "index": 11} - NON_DIEGETICS = {"name": "non_diegetic", "index": 12} - CAPTIONS = {"name": "captions", "index": 13} - DESCRIPTIONS = {"name": "descriptions", "index": 14} - METADATA = {"name": "metadata", "index": 15} - DEPENDENT = {"name": "dependent", "index": 16} - STILL_IMAGE = {"name": "still_image", "index": 17} + DEFAULT = {"name": "default", "index": 0, "indicator": "DE"} + FORCED = {"name": "forced", "index": 1, "indicator": "FO"} + + DUB = {"name": "dub", "index": 2, "indicator": "DB"} + ORIGINAL = {"name": "original", "index": 3, "indicator": "OG"} + COMMENT = {"name": "comment", "index": 4, "indicator": "CM"} + LYRICS = {"name": "lyrics", "index": 5, "indicator": "LY"} + KARAOKE = {"name": "karaoke", "index": 6, "indicator": "KA"} + HEARING_IMPAIRED = {"name": "hearing_impaired", "index": 7, "indicator": "HI"} + VISUAL_IMPAIRED = {"name": "visual_impaired", "index": 8, "indicator": "VI"} + CLEAN_EFFECTS = {"name": "clean_effects", "index": 9, "indicator": "CE"} + ATTACHED_PIC = {"name": "attached_pic", "index": 10, "indicator": "AP"} + TIMED_THUMBNAILS = {"name": "timed_thumbnails", "index": 11, "indicator": "TT"} + NON_DIEGETICS = {"name": "non_diegetic", "index": 12, "indicator": "ND"} + CAPTIONS = {"name": "captions", "index": 13, "indicator": "CA"} + DESCRIPTIONS = {"name": "descriptions", "index": 14, "indicator": "DS"} + METADATA = {"name": "metadata", "index": 15, "indicator": "MD"} + DEPENDENT = {"name": "dependent", "index": 16, "indicator": "DP"} + STILL_IMAGE = {"name": "still_image", "index": 17, "indicator": "SI"} def label(self): @@ -32,6 +32,9 @@ class TrackDisposition(Enum): def index(self): return int(self.value['index']) + def indicator(self): + return str(self.value['indicator']) + @staticmethod def toFlags(dispositionSet): diff --git a/bin/ffx_tests.py b/bin/ffx_tests.py index 185c8c7..11521ba 100755 --- a/bin/ffx_tests.py +++ b/bin/ffx_tests.py @@ -10,6 +10,7 @@ from ffx.database import databaseContext from ffx.test.helper import createMediaTestFile from ffx.test.scenario import Scenario +from ffx.tmdb_controller import TMDB_API_KEY_NOT_PRESENT_EXCEPTION @click.group() @@ -20,7 +21,7 @@ def ffx(ctx, verbose, dry_run): """FFX""" ctx.obj = {} - ctx.obj['database'] = databaseContext(databasePath=None) + ctx.obj['database'] = None ctx.obj['dry_run'] = dry_run ctx.obj['verbosity'] = verbose @@ -85,14 +86,19 @@ def run(ctx, scenario, variant): for si in Scenario.list(): - scen = Scenario.getClassReference(si)(ctx.obj) + try: + SCEN = Scenario.getClassReference(si) + scen = SCEN(ctx.obj) - if scenario and scenario != scen.getScenario(): - continue + if scenario and scenario != scen.getScenario(): + continue - ctx.obj['logger'].debug(f"Running scenario {si}") + ctx.obj['logger'].debug(f"Running scenario {si}") - scen.run() + scen.run() + + except TMDB_API_KEY_NOT_PRESENT_EXCEPTION: + ctx.obj['logger'].info(f"TMDB_API_KEY not set: Skipping {SCEN.__class__.__name__}") @ffx.command()