click-textual
Javanaut 1 year ago
parent 8ec7f9c2d1
commit 82b257d809

@ -0,0 +1,32 @@
import os
from ffx.pattern_controller import PatternController
from ffx.model.show import Base
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship, sessionmaker, Mapped, backref
filename = 'Boruto.Naruto.Next.Generations.S01E256.GerEngSub.AAC.1080p.WebDL.x264-Tanuki.mkv'
# Data 'input' variable
context = {}
# Initialize DB
homeDir = os.path.expanduser("~")
ffxVarDir = os.path.join(homeDir, '.local', 'var', 'ffx')
if not os.path.exists(ffxVarDir):
os.makedirs(ffxVarDir)
context['database_url'] = f"sqlite:///{os.path.join(ffxVarDir, 'ffx.db')}"
context['database_engine'] = create_engine(context['database_url'])
context['database_session'] = sessionmaker(bind=context['database_engine'])
Base.metadata.create_all(context['database_engine'])
pc = PatternController(context)
print(pc.matchFilename(filename))

@ -1,10 +1,10 @@
#! /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
from textual.widgets import Header, Footer, Placeholder from textual.widgets import Header, Footer, Placeholder, Label
VERSION='0.1.0' VERSION='0.1.0'
@ -35,7 +35,7 @@ MKVMERGE_METADATA_KEYS = ['BPS',
'_STATISTICS_WRITING_DATE_UTC', '_STATISTICS_WRITING_DATE_UTC',
'_STATISTICS_TAGS'] '_STATISTICS_TAGS']
FILE_EXTENSION = ['mkv', 'mp4', 'avi', 'flv', 'webm'] FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm']
COMMAND_TOKENS = ['ffmpeg', '-y', '-i'] COMMAND_TOKENS = ['ffmpeg', '-y', '-i']
@ -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):
@ -65,6 +68,14 @@ class DashboardScreen(Screen):
yield Placeholder("Dashboard Screen") yield Placeholder("Dashboard Screen")
yield Footer() yield Footer()
class WarningScreen(Screen):
def __init__(self):
super().__init__()
context = self.app.getContext()
def compose(self) -> ComposeResult:
yield Label("Warning! This file is not compliant to the defined source schema!")
yield Footer()
class SettingsScreen(Screen): class SettingsScreen(Screen):
def __init__(self): def __init__(self):
@ -87,29 +98,31 @@ class HelpScreen(Screen):
class ModesApp(App): class ModesApp(App):
BINDINGS = [ BINDINGS = [
("d", "switch_mode('dashboard')", "Dashboard"), ("q", "quit()", "Quit"),
("s", "switch_mode('settings')", "Settings"), # ("d", "switch_mode('dashboard')", "Dashboard"),
("h", "switch_mode('help')", "Help"), # ("s", "switch_mode('settings')", "Settings"),
# ("h", "switch_mode('help')", "Help"),
] ]
MODES = { MODES = {
"warning": WarningScreen,
"dashboard": DashboardScreen, "dashboard": DashboardScreen,
"settings": SettingsScreen, "settings": SettingsScreen,
"help": HelpScreen, "help": HelpScreen,
} }
def __init__(self, context = {}): def __init__(self, context = {}):
super().__init__() super().__init__()
self.context = context self.context = context
def on_mount(self) -> None: def on_mount(self) -> None:
self.switch_mode("dashboard") self.switch_mode("warning")
def getContext(self): def getContext(self):
return self.context return self.context
def executeProcess(commandSequence): def executeProcess(commandSequence):
process = subprocess.Popen(commandSequence, stdout=subprocess.PIPE, stderr=subprocess.PIPE) process = subprocess.Popen(commandSequence, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@ -354,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.
@ -363,56 +378,79 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a
Suffices will we appended to filename in case of multiple created files Suffices will we appended to filename in case of multiple created files
or if the filename has not changed.""" or if the filename has not changed."""
#startTime = time.perf_counter() startTime = time.perf_counter()
#sourcePath = paths[0] context = ctx.obj
#targetFilename = paths[1]
#if not os.path.isfile(sourcePath):
# raise click.ClickException(f"There is no file with path {sourcePath}")
#click.echo(f"src: {sourcePath} tgt: {targetFilename}") click.echo(f"\nVideo encoder: {video_encoder}")
qualityTokens = quality.split(',')
q_list = [q for q in qualityTokens if q.isnumeric()]
#click.echo(f"ve={video_encoder}") click.echo(f"Qualities: {q_list}")
#qualityTokens = quality.split(',') 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"
#q_list = [q for q in qualityTokens if q.isnumeric()] 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']}")
#click.echo(q_list) ctx.obj['perform_crop'] = (crop != 'none')
#ctx.obj['bitrates'] = {} if ctx.obj['perform_crop']:
#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"
cropTokens = crop.split(',')
#click.echo(f"a={ctx.obj['bitrates']['stereo']}") if cropTokens and len(cropTokens) == 2:
#click.echo(f"ac3={ctx.obj['bitrates']['ac3']}")
#click.echo(f"dts={ctx.obj['bitrates']['dts']}")
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
#performCrop = (crop != 'none') click.echo(f"crop start={ctx.obj['crop_start']} length={ctx.obj['crop_length']}")
#if performCrop: click.echo(f"\nRunning {len(paths) * len(q_list)} jobs")
#cropTokens = crop.split(',')
#if cropTokens and len(cropTokens) == 2: se_match = re.compile(SEASON_EPISODE_INDICATOR_MATCH)
s_match = re.compile(SEASON_INDICATOR_MATCH)
e_match = re.compile(EPISODE_INDICATOR_MATCH)
#cropStart, cropLength = crop.split(',')
#else:
#cropStart = DEFAULT_CROP_START
#cropLength = DEFAULT_CROP_LENGTH
#click.echo(f"crop start={cropStart} length={cropLength}") for sourcePath in paths:
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 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}")
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)
@ -497,15 +535,18 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a
#executeProcess(commandSequence2) #executeProcess(commandSequence2)
#click.echo('\nDONE\n') #app = ModesApp(ctx.obj)
#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')
#endTime = time.perf_counter() endTime = time.perf_counter()
#click.echo(f"Time elapsed {endTime - startTime}") click.echo(f"Time elapsed {endTime - startTime}")
app = ModesApp(ctx.obj)
app.run()
click.echo(f"app result: {app.getContext()}") # click.echo(f"app result: {app.getContext()}")

@ -1,6 +1,6 @@
import os, re, click import os, re, click
class FilenameController(): class FileProperties():
FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm'] FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm']
@ -23,11 +23,6 @@ class FilenameController():
self.__sourceFilenameExtension = '' self.__sourceFilenameExtension = ''
# Determine season and episode if present in current filename
season_digits = 2
episode_digits = 2
index_digits = 3
se_match = re.compile(FilenameController.SEASON_EPISODE_INDICATOR_MATCH) se_match = re.compile(FilenameController.SEASON_EPISODE_INDICATOR_MATCH)
e_match = re.compile(FilenameController.EPISODE_INDICATOR_MATCH) e_match = re.compile(FilenameController.EPISODE_INDICATOR_MATCH)

@ -0,0 +1,66 @@
import os, re, click
class FileProperties():
FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm']
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)'
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
def ___init__(self, sourcePath, ):
# Separate basedir, basename and extension for current source file
self.__sourceDirectory = os.path.dirname(sourcePath)
self.__sourceFilename = os.path.basename(sourcePath)
sourcePathTokens = self.__sourceFilename.split('.')
if sourcePathTokens[-1] in FilenameController.FILE_EXTENSIONS:
self.__sourceFileBasename = '.'.join(sourcePathTokens[:-1])
self.__sourceFilenameExtension = sourcePathTokens[-1]
else:
self.__sourceFileBasename = self.__sourceFilename
self.__sourceFilenameExtension = ''
se_match = re.compile(FilenameController.SEASON_EPISODE_INDICATOR_MATCH)
e_match = re.compile(FilenameController.EPISODE_INDICATOR_MATCH)
se_result = se_match.search(self.__sourceFilename)
e_result = e_match.search(self.__sourceFilename)
self.__season = -1
self.__episode = -1
file_index = 0
if se_result is not None:
self.__season = int(se_result.group(1))
self.__episode = int(se_result.group(2))
elif e_result is not None:
self.__episode = int(e_result.group(1))
else:
file_index += 1
matchingFileSubtitleDescriptors = sorted([d for d in availableFileSubtitleDescriptors if d['season'] == season and d['episode'] == episode], key=lambda d: d['stream']) if availableFileSubtitleDescriptors else []
print(f"season={season} episode={episode} file={file_index}")
# Assemble target filename tokens
targetFilenameTokens = []
targetFilenameExtension = DEFAULT_FILE_EXTENSION
if label:
targetFilenameTokens = [label]
if season > -1 and episode > -1:
targetFilenameTokens += [f"S{season:0{season_digits}d}E{episode:0{episode_digits}d}"]
elif episode > -1:
targetFilenameTokens += [f"E{episode:0{episode_digits}d}"]
else:
targetFilenameTokens += [f"{file_index:0{index_digits}d}"]
else:
targetFilenameTokens = [sourceFileBasename]
Loading…
Cancel
Save