|
|
@ -1,6 +1,6 @@
|
|
|
|
#! /usr/bin/python3
|
|
|
|
#! /usr/bin/python3
|
|
|
|
|
|
|
|
|
|
|
|
import os, sys, subprocess, json, click, time
|
|
|
|
import os, sys, subprocess, json, click, time, re
|
|
|
|
|
|
|
|
|
|
|
|
from textual.app import App, ComposeResult
|
|
|
|
from textual.app import App, ComposeResult
|
|
|
|
from textual.screen import Screen
|
|
|
|
from textual.screen import Screen
|
|
|
@ -50,6 +50,9 @@ STREAM_LAYOUT_5_1 = '5.1(side)'
|
|
|
|
STREAM_LAYOUT_STEREO = 'stereo'
|
|
|
|
STREAM_LAYOUT_STEREO = 'stereo'
|
|
|
|
STREAM_LAYOUT_6CH = '6ch'
|
|
|
|
STREAM_LAYOUT_6CH = '6ch'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SEASON_EPISODE_INDICATOR_MATCH = '([sS][0-9]+)([eE][0-9]+)'
|
|
|
|
|
|
|
|
SEASON_INDICATOR_MATCH = '([sS][0-9]+)'
|
|
|
|
|
|
|
|
EPISODE_INDICATOR_MATCH = '([eE][0-9]+)'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DashboardScreen(Screen):
|
|
|
|
class DashboardScreen(Screen):
|
|
|
@ -364,8 +367,10 @@ def streams(filename):
|
|
|
|
@click.option("-c", "--clear-metadata", is_flag=True, default=False)
|
|
|
|
@click.option("-c", "--clear-metadata", is_flag=True, default=False)
|
|
|
|
@click.option("-d", "--denoise", is_flag=True, default=False)
|
|
|
|
@click.option("-d", "--denoise", is_flag=True, default=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@click.option("-o", "--output-directory", type=str, default='')
|
|
|
|
|
|
|
|
|
|
|
|
def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, ac3_bitrate, dts_bitrate, crop, clear_metadata, default_subtitle, forced_audio, default_audio, denoise):
|
|
|
|
|
|
|
|
|
|
|
|
def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, ac3_bitrate, dts_bitrate, crop, clear_metadata, default_subtitle, forced_audio, default_audio, denoise, output_directory):
|
|
|
|
"""Batch conversion of audiovideo files in format suitable for web playback, e.g. jellyfin
|
|
|
|
"""Batch conversion of audiovideo files in format suitable for web playback, e.g. jellyfin
|
|
|
|
|
|
|
|
|
|
|
|
Files found under PATHS will be converted according to parameters.
|
|
|
|
Files found under PATHS will be converted according to parameters.
|
|
|
@ -377,67 +382,75 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a
|
|
|
|
|
|
|
|
|
|
|
|
context = ctx.obj
|
|
|
|
context = ctx.obj
|
|
|
|
|
|
|
|
|
|
|
|
for sourcePath in paths:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
click.echo(f"\nVideo encoder: {video_encoder}")
|
|
|
|
|
|
|
|
|
|
|
|
if not os.path.isfile(sourcePath):
|
|
|
|
qualityTokens = quality.split(',')
|
|
|
|
click.echo(f"There is no file with path {sourcePath}, skipping ...")
|
|
|
|
q_list = [q for q in qualityTokens if q.isnumeric()]
|
|
|
|
|
|
|
|
|
|
|
|
sourceDirectory = os.path.dirname(sourcePath)
|
|
|
|
click.echo(f"Qualities: {q_list}")
|
|
|
|
sourceFilename = os.path.basename(sourcePath)
|
|
|
|
|
|
|
|
sourcePathTokens = sourceFilename.split('.')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if sourcePathTokens[-1] in FILE_EXTENSIONS:
|
|
|
|
|
|
|
|
sourceFileBasename = '.'.join(sourcePathTokens[:-1])
|
|
|
|
|
|
|
|
sourceFilenameExtension = sourcePathTokens[-1]
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
sourceFileBasename = sourceFilename
|
|
|
|
|
|
|
|
sourceFilenameExtension = ''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
click.echo(f"dir={sourceDirectory} base={sourceFileBasename} ext={sourceFilenameExtension}")
|
|
|
|
ctx.obj['bitrates'] = {}
|
|
|
|
|
|
|
|
ctx.obj['bitrates']['stereo'] = str(stereo_bitrate) if str(stereo_bitrate).endswith('k') else f"{stereo_bitrate}k"
|
|
|
|
|
|
|
|
ctx.obj['bitrates']['ac3'] = str(ac3_bitrate) if str(ac3_bitrate).endswith('k') else f"{ac3_bitrate}k"
|
|
|
|
|
|
|
|
ctx.obj['bitrates']['dts'] = str(dts_bitrate) if str(dts_bitrate).endswith('k') else f"{dts_bitrate}k"
|
|
|
|
|
|
|
|
|
|
|
|
#click.echo(f"src: {sourcePath} tgt: {targetFilename}")
|
|
|
|
click.echo(f"Stereo bitrate: {ctx.obj['bitrates']['stereo']}")
|
|
|
|
|
|
|
|
click.echo(f"AC3 bitrate: {ctx.obj['bitrates']['ac3']}")
|
|
|
|
|
|
|
|
click.echo(f"DTS bitrate: {ctx.obj['bitrates']['dts']}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ctx.obj['perform_crop'] = (crop != 'none')
|
|
|
|
|
|
|
|
|
|
|
|
#click.echo(f"ve={video_encoder}")
|
|
|
|
if ctx.obj['perform_crop']:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cropTokens = crop.split(',')
|
|
|
|
|
|
|
|
|
|
|
|
#qualityTokens = quality.split(',')
|
|
|
|
if cropTokens and len(cropTokens) == 2:
|
|
|
|
|
|
|
|
|
|
|
|
#q_list = [q for q in qualityTokens if q.isnumeric()]
|
|
|
|
ctx.obj['crop_start'], ctx.obj['crop_length'] = crop.split(',')
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
ctx.obj['crop_start'] = DEFAULT_CROP_START
|
|
|
|
|
|
|
|
ctx.obj['crop_length'] = DEFAULT_CROP_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
#click.echo(q_list)
|
|
|
|
click.echo(f"crop start={ctx.obj['crop_start']} length={ctx.obj['crop_length']}")
|
|
|
|
|
|
|
|
|
|
|
|
#ctx.obj['bitrates'] = {}
|
|
|
|
|
|
|
|
#ctx.obj['bitrates']['stereo'] = str(stereo_bitrate) if str(stereo_bitrate).endswith('k') else f"{stereo_bitrate}k"
|
|
|
|
|
|
|
|
#ctx.obj['bitrates']['ac3'] = str(ac3_bitrate) if str(ac3_bitrate).endswith('k') else f"{ac3_bitrate}k"
|
|
|
|
|
|
|
|
#ctx.obj['bitrates']['dts'] = str(dts_bitrate) if str(dts_bitrate).endswith('k') else f"{dts_bitrate}k"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
click.echo(f"\nRunning {len(paths) * len(q_list)} jobs")
|
|
|
|
|
|
|
|
|
|
|
|
#click.echo(f"a={ctx.obj['bitrates']['stereo']}")
|
|
|
|
|
|
|
|
#click.echo(f"ac3={ctx.obj['bitrates']['ac3']}")
|
|
|
|
|
|
|
|
#click.echo(f"dts={ctx.obj['bitrates']['dts']}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
se_match = re.compile(SEASON_EPISODE_INDICATOR_MATCH)
|
|
|
|
|
|
|
|
s_match = re.compile(SEASON_INDICATOR_MATCH)
|
|
|
|
|
|
|
|
e_match = re.compile(EPISODE_INDICATOR_MATCH)
|
|
|
|
|
|
|
|
|
|
|
|
#performCrop = (crop != 'none')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for sourcePath in paths:
|
|
|
|
|
|
|
|
|
|
|
|
#if performCrop:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#cropTokens = crop.split(',')
|
|
|
|
if not os.path.isfile(sourcePath):
|
|
|
|
|
|
|
|
click.echo(f"There is no file with path {sourcePath}, skipping ...")
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sourceDirectory = os.path.dirname(sourcePath)
|
|
|
|
|
|
|
|
sourceFilename = os.path.basename(sourcePath)
|
|
|
|
|
|
|
|
sourcePathTokens = sourceFilename.split('.')
|
|
|
|
|
|
|
|
|
|
|
|
#if cropTokens and len(cropTokens) == 2:
|
|
|
|
if sourcePathTokens[-1] in FILE_EXTENSIONS:
|
|
|
|
|
|
|
|
sourceFileBasename = '.'.join(sourcePathTokens[:-1])
|
|
|
|
|
|
|
|
sourceFilenameExtension = sourcePathTokens[-1]
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
sourceFileBasename = sourceFilename
|
|
|
|
|
|
|
|
sourceFilenameExtension = ''
|
|
|
|
|
|
|
|
|
|
|
|
#cropStart, cropLength = crop.split(',')
|
|
|
|
#click.echo(f"dir={sourceDirectory} base={sourceFileBasename} ext={sourceFilenameExtension}")
|
|
|
|
#else:
|
|
|
|
|
|
|
|
#cropStart = DEFAULT_CROP_START
|
|
|
|
|
|
|
|
#cropLength = DEFAULT_CROP_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#click.echo(f"crop start={cropStart} length={cropLength}")
|
|
|
|
click.echo(f"\nProcessing file {sourcePath}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
se_result = se_match.search(sourceFilename)
|
|
|
|
|
|
|
|
s_result = s_match.search(sourceFilename)
|
|
|
|
|
|
|
|
e_result = e_match.search(sourceFilename)
|
|
|
|
|
|
|
|
|
|
|
|
#click.echo(f"\nRunning {len(q_list)} jobs")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#streamDescriptor = getStreamDescriptor(sourcePath)
|
|
|
|
#streamDescriptor = getStreamDescriptor(sourcePath)
|
|
|
@ -522,8 +535,10 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a
|
|
|
|
#executeProcess(commandSequence2)
|
|
|
|
#executeProcess(commandSequence2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app = ModesApp(ctx.obj)
|
|
|
|
#app = ModesApp(ctx.obj)
|
|
|
|
app.run()
|
|
|
|
#app.run()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#click.confirm('Warning! This file is not compliant to the defined source schema! Do you want to continue?', abort=True)
|
|
|
|
|
|
|
|
|
|
|
|
click.echo('\nDONE\n')
|
|
|
|
click.echo('\nDONE\n')
|
|
|
|
|
|
|
|
|
|
|
|