|
|
|
|
@@ -19,6 +19,7 @@ from ffx.video_encoder import VideoEncoder
|
|
|
|
|
from ffx.track_disposition import TrackDisposition
|
|
|
|
|
|
|
|
|
|
from ffx.process import executeProcess
|
|
|
|
|
from ffx.helper import filterFilename
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
VERSION='0.2.0'
|
|
|
|
|
@@ -150,8 +151,7 @@ def unmux(ctx,
|
|
|
|
|
subtitles_only):
|
|
|
|
|
|
|
|
|
|
existingSourcePaths = [p for p in paths if os.path.isfile(p)]
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"\nUnmuxing {len(existingSourcePaths)} files")
|
|
|
|
|
ctx.obj['logger'].debug(f"\nUnmuxing {len(existingSourcePaths)} files")
|
|
|
|
|
|
|
|
|
|
for sourcePath in existingSourcePaths:
|
|
|
|
|
|
|
|
|
|
@@ -169,12 +169,10 @@ def unmux(ctx,
|
|
|
|
|
targetIndicator = f"_S{season}E{episode}" if label and season != -1 and episode != -1 else ''
|
|
|
|
|
|
|
|
|
|
if label and not targetIndicator:
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"Skipping file {fp.getFilename()}: Label set but no indicator recognized")
|
|
|
|
|
ctx.obj['logger'].warning(f"Skipping file {fp.getFilename()}: Label set but no indicator recognized")
|
|
|
|
|
continue
|
|
|
|
|
else:
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"\nUnmuxing file {fp.getFilename()}\n")
|
|
|
|
|
ctx.obj['logger'].debug(f"\nUnmuxing file {fp.getFilename()}\n")
|
|
|
|
|
|
|
|
|
|
for trackDescriptor in sourceMediaDescriptor.getAllTrackDescriptors():
|
|
|
|
|
|
|
|
|
|
@@ -187,18 +185,14 @@ def unmux(ctx,
|
|
|
|
|
|
|
|
|
|
if unmuxSequence:
|
|
|
|
|
if not ctx.obj['dry_run']:
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"Executing unmuxing sequence: {' '.join(unmuxSequence)}")
|
|
|
|
|
ctx.obj['logger'].debug(f"Executing unmuxing sequence: {' '.join(unmuxSequence)}")
|
|
|
|
|
out, err, rc = executeProcess(unmuxSequence)
|
|
|
|
|
if rc:
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"Unmuxing of stream {trackDescriptor.getIndex()} failed with error ({rc}) {err}")
|
|
|
|
|
ctx.obj['logger'].error(f"Unmuxing of stream {trackDescriptor.getIndex()} failed with error ({rc}) {err}")
|
|
|
|
|
else:
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"Skipping stream with unknown codec {trackDescriptor.getCodec()}")
|
|
|
|
|
ctx.obj['logger'].warning(f"Skipping stream with unknown codec {trackDescriptor.getCodec()}")
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"Skipping File {sourcePath} ({ex})")
|
|
|
|
|
ctx.obj['logger'].warning(f"Skipping File {sourcePath} ({ex})")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ffx.command()
|
|
|
|
|
@@ -267,7 +261,7 @@ def checkUniqueDispositions(context, mediaDescriptor: MediaDescriptor):
|
|
|
|
|
@click.option('-q', '--quality', type=str, default=FfxController.DEFAULT_QUALITY, help=f"Quality settings to be used with VP9 encoder (default: {FfxController.DEFAULT_QUALITY})")
|
|
|
|
|
@click.option('-p', '--preset', type=str, default=FfxController.DEFAULT_AV1_PRESET, help=f"Quality preset to be used with AV1 encoder (default: {FfxController.DEFAULT_AV1_PRESET})")
|
|
|
|
|
|
|
|
|
|
@click.option('-s', '--stereo-bitrate', type=int, default=FfxController.DEFAULT_STEREO_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode stereo audio streams (default: {FfxController.DEFAULT_STEREO_BANDWIDTH})")
|
|
|
|
|
@click.option('-a', '--stereo-bitrate', type=int, default=FfxController.DEFAULT_STEREO_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode stereo audio streams (default: {FfxController.DEFAULT_STEREO_BANDWIDTH})")
|
|
|
|
|
@click.option('--ac3', type=int, default=FfxController.DEFAULT_AC3_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 5.1 audio streams (default: {FfxController.DEFAULT_AC3_BANDWIDTH})")
|
|
|
|
|
@click.option('--dts', type=int, default=FfxController.DEFAULT_DTS_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 6.1 audio streams (default: {FfxController.DEFAULT_DTS_BANDWIDTH})")
|
|
|
|
|
|
|
|
|
|
@@ -298,8 +292,11 @@ def checkUniqueDispositions(context, mediaDescriptor: MediaDescriptor):
|
|
|
|
|
@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)
|
|
|
|
|
|
|
|
|
|
@click.option("--no-prompt", is_flag=True, default=False)
|
|
|
|
|
@click.option("--no-signature", is_flag=True, default=False)
|
|
|
|
|
|
|
|
|
|
def convert(ctx,
|
|
|
|
|
paths,
|
|
|
|
|
@@ -331,7 +328,8 @@ def convert(ctx,
|
|
|
|
|
no_jellyfin,
|
|
|
|
|
no_pattern,
|
|
|
|
|
dont_pass_dispositions,
|
|
|
|
|
no_prompt):
|
|
|
|
|
no_prompt,
|
|
|
|
|
no_signature):
|
|
|
|
|
"""Batch conversion of audiovideo files in format suitable for web playback, e.g. jellyfin
|
|
|
|
|
|
|
|
|
|
Files found under PATHS will be converted according to parameters.
|
|
|
|
|
@@ -349,29 +347,28 @@ def convert(ctx,
|
|
|
|
|
context['use_tmdb'] = not no_tmdb
|
|
|
|
|
context['use_pattern'] = not no_pattern
|
|
|
|
|
context['no_prompt'] = no_prompt
|
|
|
|
|
context['no_signature'] = no_signature
|
|
|
|
|
|
|
|
|
|
context['import_subtitles'] = (subtitle_directory and subtitle_prefix)
|
|
|
|
|
if context['import_subtitles']:
|
|
|
|
|
context['subtitle_directory'] = subtitle_directory
|
|
|
|
|
context['subtitle_prefix'] = subtitle_prefix
|
|
|
|
|
|
|
|
|
|
# click.echo(f"\nVideo encoder: {video_encoder}")
|
|
|
|
|
ctx.obj['logger'].debug(f"\nVideo encoder: {video_encoder}")
|
|
|
|
|
|
|
|
|
|
qualityTokens = quality.split(',')
|
|
|
|
|
q_list = [q for q in qualityTokens if q.isnumeric()]
|
|
|
|
|
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"Qualities: {q_list}")
|
|
|
|
|
ctx.obj['logger'].debug(f"Qualities: {q_list}")
|
|
|
|
|
|
|
|
|
|
context['bitrates'] = {}
|
|
|
|
|
context['bitrates']['stereo'] = str(stereo_bitrate) if str(stereo_bitrate).endswith('k') else f"{stereo_bitrate}k"
|
|
|
|
|
context['bitrates']['ac3'] = str(ac3) if str(ac3).endswith('k') else f"{ac3}k"
|
|
|
|
|
context['bitrates']['dts'] = str(dts) if str(dts).endswith('k') else f"{dts}k"
|
|
|
|
|
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"Stereo bitrate: {context['bitrates']['stereo']}")
|
|
|
|
|
click.echo(f"AC3 bitrate: {context['bitrates']['ac3']}")
|
|
|
|
|
click.echo(f"DTS bitrate: {context['bitrates']['dts']}")
|
|
|
|
|
ctx.obj['logger'].debug(f"Stereo bitrate: {context['bitrates']['stereo']}")
|
|
|
|
|
ctx.obj['logger'].debug(f"AC3 bitrate: {context['bitrates']['ac3']}")
|
|
|
|
|
ctx.obj['logger'].debug(f"DTS bitrate: {context['bitrates']['dts']}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Process crop parameters
|
|
|
|
|
@@ -381,15 +378,14 @@ def convert(ctx,
|
|
|
|
|
if cTokens and len(cTokens) == 2:
|
|
|
|
|
context['crop_start'] = int(cTokens[0])
|
|
|
|
|
context['crop_length'] = int(cTokens[1])
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"Crop start={context['crop_start']} length={context['crop_length']}")
|
|
|
|
|
ctx.obj['logger'].debug(f"Crop start={context['crop_start']} length={context['crop_length']}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"\nRunning {len(existingSourcePaths) * len(q_list)} jobs")
|
|
|
|
|
ctx.obj['logger'].info(f"\nRunning {len(existingSourcePaths) * len(q_list)} jobs")
|
|
|
|
|
|
|
|
|
|
jobIndex = 0
|
|
|
|
|
|
|
|
|
|
for sourcePath in existingSourcePaths:
|
|
|
|
|
@@ -402,8 +398,7 @@ def convert(ctx,
|
|
|
|
|
sourceFileBasename = '.'.join(sourcePathTokens[:-1])
|
|
|
|
|
sourceFilenameExtension = sourcePathTokens[-1]
|
|
|
|
|
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"\nProcessing file {sourcePath}")
|
|
|
|
|
ctx.obj['logger'].info(f"\nProcessing file {sourcePath}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mediaFileProperties = FileProperties(context, sourceFilename)
|
|
|
|
|
@@ -412,8 +407,7 @@ def convert(ctx,
|
|
|
|
|
#HINT: This is None if the filename did not match anything in database
|
|
|
|
|
currentPattern = mediaFileProperties.getPattern() if context['use_pattern'] else None
|
|
|
|
|
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"Pattern matching: {'No' if currentPattern is None else 'Yes'}")
|
|
|
|
|
ctx.obj['logger'].debug(f"Pattern matching: {'No' if currentPattern is None else 'Yes'}")
|
|
|
|
|
|
|
|
|
|
# fileBasename = ''
|
|
|
|
|
|
|
|
|
|
@@ -452,13 +446,14 @@ def convert(ctx,
|
|
|
|
|
|
|
|
|
|
if context['use_tmdb']:
|
|
|
|
|
|
|
|
|
|
click.echo(f"Querying TMDB for show_id={currentShowDescriptor.getId()} season={mediaFileProperties.getSeason()} episode{mediaFileProperties.getEpisode()}")
|
|
|
|
|
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())
|
|
|
|
|
click.echo(f"tmdbEpisodeResult={tmdbEpisodeResult}")
|
|
|
|
|
ctx.obj['logger'].debug(f"tmdbEpisodeResult={tmdbEpisodeResult}")
|
|
|
|
|
|
|
|
|
|
if tmdbEpisodeResult:
|
|
|
|
|
filteredEpisodeName = filterFilename(tmdbEpisodeResult['name'])
|
|
|
|
|
sourceFileBasename = TmdbController.getEpisodeFileBasename(currentShowDescriptor.getFilenamePrefix(),
|
|
|
|
|
tmdbEpisodeResult['name'],
|
|
|
|
|
filteredEpisodeName,
|
|
|
|
|
mediaFileProperties.getSeason(),
|
|
|
|
|
mediaFileProperties.getEpisode(),
|
|
|
|
|
currentShowDescriptor.getIndexSeasonDigits(),
|
|
|
|
|
@@ -474,50 +469,45 @@ def convert(ctx,
|
|
|
|
|
mediaFileProperties.getSeason(),
|
|
|
|
|
mediaFileProperties.getEpisode())
|
|
|
|
|
|
|
|
|
|
# raise click.ClickException(f"tmd subindices: {[t.getSubIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]}")
|
|
|
|
|
# click.echo(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"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 context['use_jellyfin']:
|
|
|
|
|
# Reorder subtracks in types with default the last, then make subindices flat again
|
|
|
|
|
targetMediaDescriptor.applyJellyfinOrder()
|
|
|
|
|
# sourceMediaDescriptor.applyJellyfinOrder()
|
|
|
|
|
|
|
|
|
|
# click.echo(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()]}")
|
|
|
|
|
# raise click.Abort
|
|
|
|
|
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 ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"Input mapping tokens (2nd pass): {targetMediaDescriptor.getInputMappingTokens()}")
|
|
|
|
|
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()}")
|
|
|
|
|
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"Season={mediaFileProperties.getSeason()} Episode={mediaFileProperties.getEpisode()}")
|
|
|
|
|
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"fileBasename={sourceFileBasename}")
|
|
|
|
|
ctx.obj['logger'].debug(f"fileBasename={sourceFileBasename}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for q in q_list:
|
|
|
|
|
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"\nRunning job {jobIndex} file={sourcePath} q={q}")
|
|
|
|
|
ctx.obj['logger'].debug(f"\nRunning job {jobIndex} file={sourcePath} q={q}")
|
|
|
|
|
jobIndex += 1
|
|
|
|
|
|
|
|
|
|
extra = ['ffx'] if sourceFilenameExtension == FfxController.DEFAULT_FILE_EXTENSION else []
|
|
|
|
|
|
|
|
|
|
click.echo(f"label={label if label else 'Falsy'}")
|
|
|
|
|
click.echo(f"sourceFileBasename={sourceFileBasename}")
|
|
|
|
|
ctx.obj['logger'].debug(f"label={label if label else 'Falsy'}")
|
|
|
|
|
ctx.obj['logger'].debug(f"sourceFileBasename={sourceFileBasename}")
|
|
|
|
|
|
|
|
|
|
targetFilename = (sourceFileBasename if context['use_tmdb']
|
|
|
|
|
else mediaFileProperties.assembleTargetFileBasename(label,
|
|
|
|
|
q if len(q_list) > 1 else -1,
|
|
|
|
|
extraTokens = extra))
|
|
|
|
|
targetFileBasename = mediaFileProperties.assembleTargetFileBasename(label,
|
|
|
|
|
q if len(q_list) > 1 else -1,
|
|
|
|
|
extraTokens = extra)
|
|
|
|
|
|
|
|
|
|
#TODO #387
|
|
|
|
|
targetFilename = ((f"{sourceFileBasename}_q{q}" if len(q_list) > 1 else sourceFileBasename)
|
|
|
|
|
if context['use_tmdb'] else targetFileBasename)
|
|
|
|
|
|
|
|
|
|
targetPath = os.path.join(output_directory if output_directory else sourceDirectory, targetFilename)
|
|
|
|
|
|
|
|
|
|
# media_S01E02_S01E02
|
|
|
|
|
click.echo(f"targetPath={targetPath}")
|
|
|
|
|
#TODO: target extension anpassen
|
|
|
|
|
ctx.obj['logger'].info(f"Creating file {targetFilename}.webm")
|
|
|
|
|
|
|
|
|
|
fc.runJob(sourcePath,
|
|
|
|
|
targetPath,
|
|
|
|
|
@@ -529,8 +519,7 @@ def convert(ctx,
|
|
|
|
|
#TODO: click.confirm('Warning! This file is not compliant to the defined source schema! Do you want to continue?', abort=True)
|
|
|
|
|
|
|
|
|
|
endTime = time.perf_counter()
|
|
|
|
|
if ctx.obj['verbosity'] > 0:
|
|
|
|
|
click.echo(f"\nDONE\nTime elapsed {endTime - startTime}")
|
|
|
|
|
ctx.obj['logger'].info(f"\nDONE\nTime elapsed {endTime - startTime}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
|