124
bin/ffx.py
124
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}")
|
||||
|
||||
|
||||
@@ -122,24 +122,13 @@ 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}",
|
||||
'libopus',
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user