From 5febb96916e6803dd0db049b9d5e817f6d9465aa Mon Sep 17 00:00:00 2001 From: Maveno Date: Sun, 10 Nov 2024 16:50:13 +0100 Subject: [PATCH] #394, #406 --- bin/ffx.py | 124 +++++++++++++++++++++------------ bin/ffx/ffx_controller.py | 45 +++++------- bin/ffx/show_details_screen.py | 7 +- bin/ffx/tmdb_controller.py | 10 +++ 4 files changed, 110 insertions(+), 76 deletions(-) diff --git a/bin/ffx.py b/bin/ffx.py index 00520a8..3bc0710 100755 --- a/bin/ffx.py +++ b/bin/ffx.py @@ -14,6 +14,8 @@ from ffx.database import databaseContext from ffx.media_descriptor import MediaDescriptor from ffx.track_descriptor import TrackDescriptor +from ffx.show_descriptor import ShowDescriptor + from ffx.track_type import TrackType from ffx.video_encoder import VideoEncoder from ffx.track_disposition import TrackDisposition @@ -299,9 +301,11 @@ def checkUniqueDispositions(context, mediaDescriptor: MediaDescriptor): @click.option('--denoise-research-window', type=str, default='', help='Range to search for comparable patches on luminosity plane. Better filtering but costly.') @click.option('--denoise-chroma-research-window', type=str, default='', help='Range to search for comparable patches on chroma planes.') +@click.option('--show', type=int, default=-1, help='Set TMDB show identifier') +@click.option('--season', type=int, default=-1, help='Set season of show') +@click.option('--episode', type=int, default=-1, help='Set episode of show') @click.option("--no-tmdb", is_flag=True, default=False) -# @click.option("--no-jellyfin", is_flag=True, default=False) @click.option("--no-pattern", is_flag=True, default=False) @click.option("--dont-pass-dispositions", is_flag=True, default=False) @@ -346,6 +350,10 @@ def convert(ctx, denoise_research_window, denoise_chroma_research_window, + show, + season, + episode, + no_tmdb, # no_jellyfin, no_pattern, @@ -389,6 +397,11 @@ def convert(ctx, context['subtitle_prefix'] = subtitle_prefix + existingSourcePaths = [p for p in paths if os.path.isfile(p) and p.split('.')[-1] in FfxController.INPUT_FILE_EXTENSIONS] + + + # CLI Overrides + cliOverrides = {} if language: @@ -426,6 +439,18 @@ def convert(ctx, if forced_subtitle != -1: cliOverrides['forced_subtitle'] = forced_subtitle + if show != -1 or season != -1 or episode != -1: + if len(existingSourcePaths) > 1: + context['logger'].warning(f"Ignoring TMDB show, season, episode overrides, not supported for multiple source files") + else: + cliOverrides['tmdb'] = {} + if show != -1: + cliOverrides['tmdb']['show'] = show + if season != -1: + cliOverrides['tmdb']['season'] = season + if episode != -1: + cliOverrides['tmdb']['episode'] = episode + if cliOverrides: context['overrides'] = cliOverrides @@ -468,7 +493,7 @@ def convert(ctx, tc = TmdbController() if context['use_tmdb'] else None - existingSourcePaths = [p for p in paths if os.path.isfile(p) and p.split('.')[-1] in FfxController.INPUT_FILE_EXTENSIONS] + ctx.obj['logger'].info(f"\nRunning {len(existingSourcePaths) * len(q_list)} jobs") jobIndex = 0 @@ -487,6 +512,15 @@ def convert(ctx, mediaFileProperties = FileProperties(context, sourceFilename) + + #HINT: -1 if not set + showSeason = (cliOverrides['tmdb']['season'] if 'tmdb' in cliOverrides.keys() + and 'season' in cliOverrides['tmdb'] else mediaFileProperties.getSeason()) + showEpisode = (cliOverrides['tmdb']['episode'] if 'tmdb' in cliOverrides.keys() + and 'episode' in cliOverrides['tmdb'] else mediaFileProperties.getEpisode()) + ctx.obj['logger'].debug(f"Season={showSeason} Episode={showEpisode}") + + sourceMediaDescriptor = mediaFileProperties.getMediaDescriptor() #HINT: This is None if the filename did not match anything in database @@ -494,80 +528,84 @@ def convert(ctx, ctx.obj['logger'].debug(f"Pattern matching: {'No' if currentPattern is None else 'Yes'}") - # fileBasename = '' - + # Setup FfxController accordingly depending on pattern matching is enabled and a pattern was matched if currentPattern is None: - - # Case no pattern matching - - # fileBasename = currentShowDescriptor.getFilenamePrefix() checkUniqueDispositions(context, sourceMediaDescriptor) + currentShowDescriptor = None if context['import_subtitles']: sourceMediaDescriptor.importSubtitles(context['subtitle_directory'], context['subtitle_prefix'], - mediaFileProperties.getSeason(), - mediaFileProperties.getEpisode()) + showSeason, + showEpisode) if cliOverrides: sourceMediaDescriptor.applyOverrides(cliOverrides) - #YOLO fc = FfxController(context, sourceMediaDescriptor) - else: - - # Case pattern matching - targetMediaDescriptor = currentPattern.getMediaDescriptor(ctx.obj) - checkUniqueDispositions(context, targetMediaDescriptor) - - currentShowDescriptor = currentPattern.getShowDescriptor(ctx.obj) - - - if context['use_tmdb']: - - ctx.obj['logger'].debug(f"Querying TMDB for show_id={currentShowDescriptor.getId()} season={mediaFileProperties.getSeason()} episode{mediaFileProperties.getEpisode()}") - tmdbEpisodeResult = tc.queryEpisode(currentShowDescriptor.getId(), mediaFileProperties.getSeason(), mediaFileProperties.getEpisode()) - ctx.obj['logger'].debug(f"tmdbEpisodeResult={tmdbEpisodeResult}") - - if tmdbEpisodeResult: - filteredEpisodeName = filterFilename(tmdbEpisodeResult['name']) - sourceFileBasename = TmdbController.getEpisodeFileBasename(currentShowDescriptor.getFilenamePrefix(), - filteredEpisodeName, - mediaFileProperties.getSeason(), - mediaFileProperties.getEpisode(), - currentShowDescriptor.getIndexSeasonDigits(), - currentShowDescriptor.getIndexEpisodeDigits(), - currentShowDescriptor.getIndicatorSeasonDigits(), - currentShowDescriptor.getIndicatorEpisodeDigits()) - else: - sourceFileBasename = currentShowDescriptor.getFilenamePrefix() if context['import_subtitles']: targetMediaDescriptor.importSubtitles(context['subtitle_directory'], context['subtitle_prefix'], - mediaFileProperties.getSeason(), - mediaFileProperties.getEpisode()) + showSeason, + showEpisode) ctx.obj['logger'].debug(f"tmd subindices: {[t.getIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getSubIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getDispositionFlag(TrackDisposition.DEFAULT) for t in targetMediaDescriptor.getAllTrackDescriptors()]}") - if cliOverrides: targetMediaDescriptor.applyOverrides(cliOverrides) - ctx.obj['logger'].debug(f"tmd subindices: {[t.getIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getSubIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getDispositionFlag(TrackDisposition.DEFAULT) for t in targetMediaDescriptor.getAllTrackDescriptors()]}") ctx.obj['logger'].debug(f"Input mapping tokens (2nd pass): {targetMediaDescriptor.getInputMappingTokens()}") fc = FfxController(context, targetMediaDescriptor, sourceMediaDescriptor) - ctx.obj['logger'].debug(f"Season={mediaFileProperties.getSeason()} Episode={mediaFileProperties.getEpisode()}") + + # Assemble target filename accordingly depending on TMDB lookup is enabled + #HINT: -1 if not set + showId = cliOverrides['tmdb']['show'] if 'tmdb' in cliOverrides.keys() and 'show' in cliOverrides['tmdb'] else (-1 if currentShowDescriptor is None else currentShowDescriptor.getId()) + + if context['use_tmdb'] and showId != -1 and showSeason != -1 and showEpisode != -1: + + ctx.obj['logger'].debug(f"Querying TMDB for show_id={showId} season={showSeason} episode{showEpisode}") + + if currentPattern is None: + sName, showYear = tc.getShowNameAndYear(showId) + showName = filterFilename(sName) + showFilenamePrefix = f"{showName} ({str(showYear)})" + indexSeasonDigits = ShowDescriptor.DEFAULT_INDEX_SEASON_DIGITS + indexEpisodeDigits = ShowDescriptor.DEFAULT_INDEX_EPISODE_DIGITS + indicatorSeasonDigits = ShowDescriptor.DEFAULT_INDICATOR_SEASON_DIGITS + indicatorEpisodeDigits = ShowDescriptor.DEFAULT_INDICATOR_EPISODE_DIGITS + else: + showFilenamePrefix = currentShowDescriptor.getFilenamePrefix() + indexSeasonDigits = currentShowDescriptor.getIndexSeasonDigits() + indexEpisodeDigits = currentShowDescriptor.getIndexEpisodeDigits() + indicatorSeasonDigits = currentShowDescriptor.getIndicatorSeasonDigits() + indicatorEpisodeDigits = currentShowDescriptor.getIndicatorEpisodeDigits() + + tmdbEpisodeResult = tc.queryEpisode(showId, showSeason, showEpisode) + + ctx.obj['logger'].debug(f"tmdbEpisodeResult={tmdbEpisodeResult}") + + if tmdbEpisodeResult: + filteredEpisodeName = filterFilename(tmdbEpisodeResult['name']) + sourceFileBasename = TmdbController.getEpisodeFileBasename(showFilenamePrefix, + filteredEpisodeName, + showSeason, + showEpisode, + indexSeasonDigits, + indexEpisodeDigits, + indicatorSeasonDigits, + indicatorEpisodeDigits) + ctx.obj['logger'].debug(f"fileBasename={sourceFileBasename}") diff --git a/bin/ffx/ffx_controller.py b/bin/ffx/ffx_controller.py index 9c69f3c..8e6756c 100644 --- a/bin/ffx/ffx_controller.py +++ b/bin/ffx/ffx_controller.py @@ -122,23 +122,12 @@ class FfxController(): audioTokens = [] - #sourceAudioTrackDescriptors = [smd for smd in self.__sourceMediaDescriptor.getAllTrackDescriptors() if smd.getType() == TrackType.AUDIO] - # targetAudioTrackDescriptors = [rtd for rtd in self.__targetMediaDescriptor.getReorderedTrackDescriptors() if rtd.getType() == TrackType.AUDIO] targetAudioTrackDescriptors = [td for td in self.__targetMediaDescriptor.getAllTrackDescriptors() if td.getType() == TrackType.AUDIO] trackSubIndex = 0 for trackDescriptor in targetAudioTrackDescriptors: - # Calculate source sub index - #changedTargetTrackDescriptor : TrackDescriptor = targetAudioTrackDescriptors[trackDescriptor.getIndex()] - #changedTargetTrackSourceIndex = changedTargetTrackDescriptor.getSourceIndex() - #sourceSubIndex = sourceAudioTrackDescriptors[changedTargetTrackSourceIndex].getSubIndex() - trackAudioLayout = trackDescriptor.getAudioLayout() - - #TODO: Sollte nicht die sub index unverändert bleiben wenn jellyfin reordering angewendet wurde? - # siehe auch: MediaDescriptor.getInputMappingTokens() - #trackSubIndex = trackDescriptor.getSubIndex() if trackAudioLayout == AudioLayout.LAYOUT_6_1: audioTokens += [f"-c:a:{trackSubIndex}", @@ -180,10 +169,6 @@ class FfxController(): sourceTrackDescriptors = ([] if self.__sourceMediaDescriptor is None else self.__sourceMediaDescriptor.getAllTrackDescriptors()) - # if not self.__sourceMediaDescriptor is None: - # sourceTrackDescriptors = self.__sourceMediaDescriptor.getAllTrackDescriptors() - # else: - # sourceTrackDescriptors = [] dispositionTokens = [] @@ -274,12 +259,9 @@ class FfxController(): + self.__targetMediaDescriptor.getInputMappingTokens() + self.generateDispositionTokens()) - if not self.__sourceMediaDescriptor is None or 'overrides' in self.__context.keys(): - commandSequence += self.generateMetadataTokens() - - # if denoise: - # commandSequence += self.generateDenoiseTokens() - commandSequence1 += self.__context['denoiser'].generateDenoiseTokens() + # Optional tokens + commandSequence += self.generateMetadataTokens() + commandSequence += self.__context['denoiser'].generateDenoiseTokens() commandSequence += (self.generateAudioEncodingTokens() + self.generateAV1Tokens(int(quality), int(preset)) @@ -301,8 +283,16 @@ class FfxController(): if videoEncoder == VideoEncoder.VP9: commandSequence1 = (commandTokens - + self.__targetMediaDescriptor.getInputMappingTokens(only_video=True) - + self.generateVP9Pass1Tokens(int(quality))) + + self.__targetMediaDescriptor.getInputMappingTokens(only_video=True)) + + # Optional tokens + #NOTE: Filters and so needs to run on the first pass as well, as here + # the required bitrate for the second run is determined and recorded + # TODO: Results seems to be slightly better with first pass omitted, + # Confirm or find better filter settings for 2-pass + # commandSequence1 += self.__context['denoiser'].generateDenoiseTokens() + + commandSequence1 += self.generateVP9Pass1Tokens(int(quality)) if self.__context['perform_crop']: commandSequence1 += self.generateCropTokens() @@ -322,11 +312,8 @@ class FfxController(): + self.__targetMediaDescriptor.getInputMappingTokens() + self.generateDispositionTokens()) - if not self.__sourceMediaDescriptor is None or 'overrides' in self.__context.keys(): - commandSequence2 += self.generateMetadataTokens() - - # if denoise: - # commandSequence2 += self.generateDenoiseTokens() + # Optional tokens + commandSequence2 += self.generateMetadataTokens() commandSequence2 += self.__context['denoiser'].generateDenoiseTokens() commandSequence2 += self.generateVP9Pass2Tokens(int(quality)) + self.generateAudioEncodingTokens() @@ -348,7 +335,7 @@ class FfxController(): def createEmptyFile(self, - path: str = 'output.mp4', + path: str = 'empty.mkv', sizeX: int = 1280, sizeY: int = 720, rate: int = 25, diff --git a/bin/ffx/show_details_screen.py b/bin/ffx/show_details_screen.py index f7e0626..3a38569 100644 --- a/bin/ffx/show_details_screen.py +++ b/bin/ffx/show_details_screen.py @@ -354,8 +354,7 @@ class ShowDetailsScreen(Screen): showDescriptor = self.getShowDescriptorFromInput() if not showDescriptor is None: - showResult = self.__tc.queryShow(showDescriptor.getId()) - firstAirDate = datetime.strptime(showResult['first_air_date'], '%Y-%m-%d') + showName, showYear = self.__tc.getShowNameAndYear(showDescriptor.getId()) - self.query_one("#name_input", Input).value = filterFilename(showResult['name']) - self.query_one("#year_input", Input).value = str(firstAirDate.year) + self.query_one("#name_input", Input).value = filterFilename(showName) + self.query_one("#year_input", Input).value = str(showYear) diff --git a/bin/ffx/tmdb_controller.py b/bin/ffx/tmdb_controller.py index 365287f..a552c0c 100644 --- a/bin/ffx/tmdb_controller.py +++ b/bin/ffx/tmdb_controller.py @@ -1,4 +1,6 @@ import os, click, requests, json, time, logging +from datetime import datetime + class TMDB_REQUEST_EXCEPTION(Exception): def __init__(self, statusCode, statusMessage): @@ -95,6 +97,14 @@ class TmdbController(): return self.getTmdbRequest(tmdbUrl) + def getShowNameAndYear(self, showId: int): + + showResult = self.queryShow(int(showId)) + firstAirDate = datetime.strptime(showResult['first_air_date'], '%Y-%m-%d') + + return str(showResult['name']), int(firstAirDate.year) + + def queryEpisode(self, showId, season, episode): """ First level keys in the response object: