4 Commits

Author SHA1 Message Date
Maveno
7d7e43c6f0 rework descriptors raw 2024-10-06 09:58:37 +02:00
Maveno
1c9f67e47a nightl 2024-10-05 22:41:57 +02:00
Maveno
eaee3b34da nightl 2024-10-04 23:45:25 +02:00
Maveno
5e017a8373 nightl 2024-10-03 23:22:32 +02:00
27 changed files with 1421 additions and 603 deletions

View File

@@ -4,6 +4,12 @@ import os, sys, subprocess, json, click, time, re
from ffx.ffx_app import FfxApp from ffx.ffx_app import FfxApp
from ffx.database import databaseContext
from ffx.media_descriptor import MediaDescriptor
from ffx.file_properties import FileProperties
VERSION='0.1.0' VERSION='0.1.0'
DEFAULT_VIDEO_ENCODER = 'vp9' DEFAULT_VIDEO_ENCODER = 'vp9'
@@ -41,11 +47,6 @@ STREAM_TYPE_VIDEO = 'video'
STREAM_TYPE_AUDIO = 'audio' STREAM_TYPE_AUDIO = 'audio'
STREAM_TYPE_SUBTITLE = 'subtitle' STREAM_TYPE_SUBTITLE = 'subtitle'
STREAM_LAYOUT_6_1 = '6.1'
STREAM_LAYOUT_5_1 = '5.1(side)'
STREAM_LAYOUT_STEREO = 'stereo'
STREAM_LAYOUT_6CH = '6ch'
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)' SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)'
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)' EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
SEASON_EPISODE_STREAM_LANGUAGE_MATCH = '[sS]([0-9]+)[eE]([0-9]+)_([0-9]+)_([a-z]{3})' SEASON_EPISODE_STREAM_LANGUAGE_MATCH = '[sS]([0-9]+)[eE]([0-9]+)_([0-9]+)_([a-z]{3})'
@@ -53,119 +54,6 @@ SEASON_EPISODE_STREAM_LANGUAGE_MATCH = '[sS]([0-9]+)[eE]([0-9]+)_([0-9]+)_([a-z]
SUBTITLE_FILE_EXTENSION = 'vtt' SUBTITLE_FILE_EXTENSION = 'vtt'
def executeProcess(commandSequence):
process = subprocess.Popen(commandSequence, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, error = process.communicate()
return output.decode('utf-8'), error.decode('utf-8'), process.returncode
#[{'index': 0, 'codec_name': 'vp9', 'codec_long_name': 'Google VP9', 'profile': 'Profile 0', 'codec_type': 'video', 'codec_tag_string': '[0][0][0][0]', 'codec_tag': '0x0000', 'width': 1920, 'height': 1080, 'coded_width': 1920, 'coded_height': 1080, 'closed_captions': 0, 'film_grain': 0, 'has_b_frames': 0, 'sample_aspect_ratio': '1:1', 'display_aspect_ratio': '16:9', 'pix_fmt': 'yuv420p', 'level': -99, 'color_range': 'tv', 'chroma_location': 'left', 'field_order': 'progressive', 'refs': 1, 'r_frame_rate': '24000/1001', 'avg_frame_rate': '24000/1001', 'time_base': '1/1000', 'start_pts': 0, 'start_time': '0.000000', 'disposition': {'default': 1, 'dub': 0, 'original': 0, 'comment': 0, 'lyrics': 0, 'karaoke': 0, 'forced': 0, 'hearing_impaired': 0, 'visual_impaired': 0, 'clean_effects': 0, 'attached_pic': 0, 'timed_thumbnails': 0, 'non_diegetic': 0, 'captions': 0, 'descriptions': 0, 'metadata': 0, 'dependent': 0, 'still_image': 0}, 'tags': {'BPS': '7974017', 'NUMBER_OF_FRAMES': '34382', 'NUMBER_OF_BYTES': '1429358655', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 libvpx-vp9', 'DURATION': '00:23:54.016000000'}}]
#[{'index': 1, 'codec_name': 'opus', 'codec_long_name': 'Opus (Opus Interactive Audio Codec)', 'codec_type': 'audio', 'codec_tag_string': '[0][0][0][0]', 'codec_tag': '0x0000', 'sample_fmt': 'fltp', 'sample_rate': '48000', 'channels': 2, 'channel_layout': 'stereo', 'bits_per_sample': 0, 'initial_padding': 312, 'r_frame_rate': '0/0', 'avg_frame_rate': '0/0', 'time_base': '1/1000', 'start_pts': -7, 'start_time': '-0.007000', 'extradata_size': 19, 'disposition': {'default': 1, 'dub': 0, 'original': 0, 'comment': 0, 'lyrics': 0, 'karaoke': 0, 'forced': 0, 'hearing_impaired': 0, 'visual_impaired': 0, 'clean_effects': 0, 'attached_pic': 0, 'timed_thumbnails': 0, 'non_diegetic': 0, 'captions': 0, 'descriptions': 0, 'metadata': 0, 'dependent': 0, 'still_image': 0}, 'tags': {'language': 'jpn', 'title': 'Japanisch', 'BPS': '128000', 'NUMBER_OF_FRAMES': '61763', 'NUMBER_OF_BYTES': '22946145', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 libopus', 'DURATION': '00:23:54.141000000'}}]
#[{'index': 2, 'codec_name': 'webvtt', 'codec_long_name': 'WebVTT subtitle', 'codec_type': 'subtitle', 'codec_tag_string': '[0][0][0][0]', 'codec_tag': '0x0000', 'r_frame_rate': '0/0', 'avg_frame_rate': '0/0', 'time_base': '1/1000', 'start_pts': -7, 'start_time': '-0.007000', 'duration_ts': 1434141, 'duration': '1434.141000', 'disposition': {'default': 1, 'dub': 0, 'original': 0, 'comment': 0, 'lyrics': 0, 'karaoke': 0, 'forced': 0, 'hearing_impaired': 0, 'visual_impaired': 0, 'clean_effects': 0, 'attached_pic': 0, 'timed_thumbnails': 0, 'non_diegetic': 0, 'captions': 0, 'descriptions': 0, 'metadata': 0, 'dependent': 0, 'still_image': 0}, 'tags': {'language': 'ger', 'title': 'Deutsch [Full]', 'BPS': '118', 'NUMBER_OF_FRAMES': '300', 'NUMBER_OF_BYTES': '21128', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 webvtt', 'DURATION': '00:23:54.010000000'}}, {'index': 3, 'codec_name': 'webvtt', 'codec_long_name': 'WebVTT subtitle', 'codec_type': 'subtitle', 'codec_tag_string': '[0][0][0][0]', 'codec_tag': '0x0000', 'r_frame_rate': '0/0', 'avg_frame_rate': '0/0', 'time_base': '1/1000', 'start_pts': -7, 'start_time': '-0.007000', 'duration_ts': 1434141, 'duration': '1434.141000', 'disposition': {'default': 0, 'dub': 0, 'original': 0, 'comment': 0, 'lyrics': 0, 'karaoke': 0, 'forced': 0, 'hearing_impaired': 0, 'visual_impaired': 0, 'clean_effects': 0, 'attached_pic': 0, 'timed_thumbnails': 0, 'non_diegetic': 0, 'captions': 0, 'descriptions': 0, 'metadata': 0, 'dependent': 0, 'still_image': 0}, 'tags': {'language': 'eng', 'title': 'Englisch [Full]', 'BPS': '101', 'NUMBER_OF_FRAMES': '276', 'NUMBER_OF_BYTES': '16980', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 webvtt', 'DURATION': '00:23:53.230000000'}}]
def getStreamData(filepath):
"""Returns ffprobe stream data as array with elements according to the following example
{
"index": 4,
"codec_name": "hdmv_pgs_subtitle",
"codec_long_name": "HDMV Presentation Graphic Stream subtitles",
"codec_type": "subtitle",
"codec_tag_string": "[0][0][0][0]",
"codec_tag": "0x0000",
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/1000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 1421035,
"duration": "1421.035000",
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0,
"non_diegetic": 0,
"captions": 0,
"descriptions": 0,
"metadata": 0,
"dependent": 0,
"still_image": 0
},
"tags": {
"language": "ger",
"title": "German Full"
}
}
"""
ffprobeOutput, ffprobeError, returnCode = executeProcess(["ffprobe",
"-show_streams",
"-of", "json",
filepath])
if 'Invalid data found when processing input' in ffprobeError:
raise Exception(f"File {filepath} does not contain valid stream data")
if returnCode != 0:
raise Exception(f"ffprobe returned with error {returnCode}")
return json.loads(ffprobeOutput)['streams']
def getStreamDescriptor(filename):
streamData = getStreamData(filename)
descriptor = {}
descriptor['video'] = []
descriptor['audio'] = []
descriptor['subtitle'] = []
for subStream in streamData:
if subStream['codec_type'] in ['video', 'audio', 'subtitle']:
if not 'disposition' in subStream.keys():
subStream['disposition'] = {}
if not 'default' in subStream['disposition'].keys():
subStream['disposition']['default'] = 0
if not 'forced' in subStream['disposition'].keys():
subStream['disposition']['forced'] = 0
if not 'tags' in subStream.keys():
subStream['tags'] = {}
if not 'language' in subStream['tags'].keys():
subStream['tags']['language'] = 'undefined'
if not 'title' in subStream['tags'].keys():
subStream['tags']['title'] = 'undefined'
if subStream['codec_type'] == STREAM_TYPE_AUDIO:
if 'channel_layout' in subStream.keys():
subStream['audio_layout'] = subStream['channel_layout']
elif subStream['channels'] == 6:
subStream['audio_layout'] = STREAM_LAYOUT_6CH
else:
subStream['audio_layout'] = 'undefined'
descriptor[subStream['codec_type']].append(subStream)
descriptor[subStream['codec_type']][-1]['sub_index'] = len(descriptor[subStream['codec_type']]) - 1
return descriptor
def getModifiedStreamOrder(length, last): def getModifiedStreamOrder(length, last):
"""This is jellyfin specific as the last stream in the order is set as default""" """This is jellyfin specific as the last stream in the order is set as default"""
@@ -232,38 +120,39 @@ def generateOutputTokens(filepath, format, ext):
def generateAudioEncodingTokens(context, index, layout): def generateAudioEncodingTokens(context, index, layout):
"""Generates ffmpeg options for one output audio stream including channel remapping, codec and bitrate""" """Generates ffmpeg options for one output audio stream including channel remapping, codec and bitrate"""
pass
if layout == STREAM_LAYOUT_6_1: #
return [f"-c:a:{index}", # if layout == STREAM_LAYOUT_6_1:
'libopus', # return [f"-c:a:{index}",
f"-filter:a:{index}", # 'libopus',
'channelmap=channel_layout=6.1', # f"-filter:a:{index}",
f"-b:a:{index}", # 'channelmap=channel_layout=6.1',
context['bitrates']['dts']] # f"-b:a:{index}",
# context['bitrates']['dts']]
elif layout == STREAM_LAYOUT_5_1: #
return [f"-c:a:{index}", # elif layout == STREAM_LAYOUT_5_1:
'libopus', # return [f"-c:a:{index}",
f"-filter:a:{index}", # 'libopus',
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1", # f"-filter:a:{index}",
f"-b:a:{index}", # "channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
context['bitrates']['ac3']] # f"-b:a:{index}",
# context['bitrates']['ac3']]
elif layout == STREAM_LAYOUT_STEREO: #
return [f"-c:a:{index}", # elif layout == STREAM_LAYOUT_STEREO:
'libopus', # return [f"-c:a:{index}",
f"-b:a:{index}", # 'libopus',
context['bitrates']['stereo']] # f"-b:a:{index}",
# context['bitrates']['stereo']]
elif layout == STREAM_LAYOUT_6CH: #
return [f"-c:a:{index}", # elif layout == STREAM_LAYOUT_6CH:
'libopus', # return [f"-c:a:{index}",
f"-filter:a:{index}", # 'libopus',
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1", # f"-filter:a:{index}",
f"-b:a:{index}", # "channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
context['bitrates']['ac3']] # f"-b:a:{index}",
else: # context['bitrates']['ac3']]
return [] # else:
# return []
def generateClearTokens(streams): def generateClearTokens(streams):
@@ -334,8 +223,9 @@ def searchSubtitleFiles(dir, prefix):
@click.pass_context @click.pass_context
def ffx(ctx): def ffx(ctx):
"""FFX""" """FFX"""
ctx.obj = {} ctx.obj = {}
pass ctx.obj['database'] = databaseContext()
# Define a subcommand # Define a subcommand
@@ -351,61 +241,79 @@ def help():
click.echo(f"Usage: ffx [input file] [output file] [vp9|av1] [q=[nn[,nn,...]]] [p=nn] [a=nnn[k]] [ac3=nnn[k]] [dts=nnn[k]] [crop]") click.echo(f"Usage: ffx [input file] [output file] [vp9|av1] [q=[nn[,nn,...]]] [p=nn] [a=nnn[k]] [ac3=nnn[k]] [dts=nnn[k]] [crop]")
@click.argument('filename', nargs=1)
@ffx.command()
def streams(filename):
try:
sd = getStreamDescriptor(filename)
except Exception as ex:
raise click.ClickException(f"This file does not contain any audiovisual data: {ex}")
for d in sd:
click.echo(f"{d['codec_name']}{' (' + str(d['channels']) + ')' if d['codec_type'] == 'audio' else ''}")
@ffx.command() @ffx.command()
@click.pass_context @click.pass_context
@click.argument('filename', nargs=1)
def inspect(ctx, filename):
@click.argument('paths', nargs=-1) # if 'database' not in ctx.obj.keys():
@click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix') # ctx.obj['database'] = databaseContext()
@click.option('-sd', '--subtitle-directory', type=str, default='', help='Load subtitles from here') try:
@click.option('-sp', '--subtitle-prefix', type=str, default='', help='Subtitle filename prefix')
@click.option("-o", "--output-directory", type=str, default='') fp = FileProperties(ctx.obj, filename)
md = fp.getMediaDescriptor()
@click.option("--dry-run", is_flag=True, default=False) print(md.getTags())
for at in md.getAudioTracks():
print(f"Audio: {at.getLanguage()} {'|'.join([f"{k}={v}" for (k,v) in at.getTags().items()])}")
for st in md.getSubtitleTracks():
print(f"Subtitle: {st.getLanguage()} {'|'.join([[f"{k}={v}" for (k,v) in st.getTags().items()]])}")
except Exception as ex:
raise click.ClickException(f"This file does not contain any audiovisual data: {ex}")
# for d in sd:
# click.echo(f"{d['codec_name']}{' (' + str(d['channels']) + ')' if d['codec_type'] == 'audio' else ''}")
def unmux(ctx,
label,
paths,
subtitle_directory,
subtitle_prefix,
output_directory,
dry_run):
existingSourcePaths = [p for p in paths if os.path.isfile(p)] # @ffx.command()
click.echo(f"\nUnmuxing {len(existingSourcePaths)} files") # @click.pass_context
#
for sourcePath in existingSourcePaths: # @click.argument('paths', nargs=-1)
# @click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix')
sd = getStreamDescriptor(sourcePath) #
# @click.option('-sd', '--subtitle-directory', type=str, default='', help='Load subtitles from here')
print(f"\nFile {sourcePath}\n") # @click.option('-sp', '--subtitle-prefix', type=str, default='', help='Subtitle filename prefix')
#
for v in sd['video']: # @click.option("-o", "--output-directory", type=str, default='')
#
if v['codec_name'] == 'h264': # @click.option("--dry-run", is_flag=True, default=False)
#
commandSequence = ['ffmpeg', '-i', sourcePath, '-map', '0:v:0', '-c', 'copy', '-f', 'h264'] #
executeProcess() # def unmux(ctx,
# label,
for a in sd['audio']: # paths,
print(f"A: {a}\n") # subtitle_directory,
for s in sd['subtitle']: # subtitle_prefix,
print(f"S: {s}\n") # output_directory,
# dry_run):
#
# existingSourcePaths = [p for p in paths if os.path.isfile(p)]
# click.echo(f"\nUnmuxing {len(existingSourcePaths)} files")
#
# for sourcePath in existingSourcePaths:
#
# sd = getStreamDescriptor(sourcePath)
#
# print(f"\nFile {sourcePath}\n")
#
# for v in sd['video']:
#
# if v['codec_name'] == 'h264':
#
# commandSequence = ['ffmpeg', '-i', sourcePath, '-map', '0:v:0', '-c', 'copy', '-f', 'h264']
# executeProcess()
#
# for a in sd['audio']:
# print(f"A: {a}\n")
# for s in sd['subtitle']:
# print(f"S: {s}\n")
@@ -415,6 +323,9 @@ def unmux(ctx,
def shows(ctx): def shows(ctx):
# if 'database' not in ctx.obj.keys():
# ctx.obj['database'] = databaseContext()
app = FfxApp(ctx.obj) app = FfxApp(ctx.obj)
app.run() app.run()
@@ -502,6 +413,10 @@ def convert(ctx,
context = ctx.obj context = ctx.obj
if 'database' not in context.keys():
context['database'] = databaseContext()
click.echo(f"\nVideo encoder: {video_encoder}") click.echo(f"\nVideo encoder: {video_encoder}")
qualityTokens = quality.split(',') qualityTokens = quality.split(',')

46
bin/ffx/audio_layout.py Normal file
View File

@@ -0,0 +1,46 @@
from enum import Enum
from .track_type import TrackType
class AudioLayout(Enum):
LAYOUT_STEREO = {"layout": "stereo", "index": 1}
LAYOUT_5_1 = {"layout": "5.1(side)", "index": 2}
LAYOUT_6_1 = {"layout": "6.1", "index": 3}
LAYOUT_7_1 = {"layout": "7.1", "index": 4} #TODO: Does this exist?
LAYOUT_6CH = {"layout": "6ch", "index": 5}
LAYOUT_UNDEFINED = {"layout": "undefined", "index": 0}
def layout(self):
"""Returns the layout as string"""
return self.value['layout']
def index(self):
"""Returns the layout as string"""
return self.value['layout']
def identify(self, streamObj):
FFPROBE_LAYOUT_KEY = 'channel_layout'
FFPROBE_CHANNELS_KEY = 'channels'
FFPROBE_CODEC_TYPE_KEY = 'codec_type'
if (type(streamObj) is not dict
or FFPROBE_CODEC_TYPE_KEY not in streamObj.keys()
or streamObj[FFPROBE_CODEC_TYPE_KEY] != TrackType.AUDIO.label()):
raise Exception('Not an ffprobe audio stream object')
if FFPROBE_LAYOUT_KEY in streamObj.keys():
matchingLayouts = [l for l in AudioLayout if l.value['layout'] == streamObj[FFPROBE_LAYOUT_KEY]]
if matchingLayouts:
return matchingLayouts[0]
if (FFPROBE_CHANNELS_KEY in streamObj.keys()
and int(streamObj[FFPROBE_CHANNELS_KEY]) == 6):
return AudioLayout.LAYOUT_6CH
return AudioLayout.LAYOUT_UNDEFINED

41
bin/ffx/database.py Normal file
View File

@@ -0,0 +1,41 @@
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from ffx.model.show import Base, Show
from ffx.model.pattern import Pattern
from ffx.model.track import Track
from ffx.model.media_tag import MediaTag
from ffx.model.track_tag import TrackTag
def databaseContext():
databaseContext = {}
# Initialize DB
homeDir = os.path.expanduser("~")
ffxVarDir = os.path.join(homeDir, '.local', 'var', 'ffx')
if not os.path.exists(ffxVarDir):
os.makedirs(ffxVarDir)
databaseContext['url'] = f"sqlite:///{os.path.join(ffxVarDir, 'ffx.db')}"
databaseContext['engine'] = create_engine(databaseContext['url'])
databaseContext['session'] = sessionmaker(bind=databaseContext['engine'])
Base.metadata.create_all(databaseContext['engine'])
# isSyncronuous = False
# while not isSyncronuous:
# while True:
# try:
# with databaseContext['database_engine'].connect() as connection:
# connection.execute(sqlalchemy.text('PRAGMA foreign_keys=ON;'))
# #isSyncronuous = True
# break
# except sqlite3.OperationalError:
# time.sleep(0.1)
return databaseContext

View File

@@ -6,9 +6,12 @@ from textual.widgets import Header, Footer, Placeholder, Label
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from ffx.model.show import Base, Show # from ffx.model.show import Base, Show
from ffx.model.pattern import Pattern # from ffx.model.pattern import Pattern
from ffx.model.track import Track # from ffx.model.track import Track
# from ffx.model.media_tag import MediaTag
# from ffx.model.track_tag import TrackTag
from .shows_screen import ShowsScreen from .shows_screen import ShowsScreen
from .warning_screen import WarningScreen from .warning_screen import WarningScreen
@@ -32,29 +35,6 @@ class FfxApp(App):
# Data 'input' variable # Data 'input' variable
self.context = context self.context = context
# Initialize DB
homeDir = os.path.expanduser("~")
ffxVarDir = os.path.join(homeDir, '.local', 'var', 'ffx')
if not os.path.exists(ffxVarDir):
os.makedirs(ffxVarDir)
self.context['database_url'] = f"sqlite:///{os.path.join(ffxVarDir, 'ffx.db')}"
self.context['database_engine'] = create_engine(self.context['database_url'])
self.context['database_session'] = sessionmaker(bind=self.context['database_engine'])
Base.metadata.create_all(self.context['database_engine'])
# isSyncronuous = False
# while not isSyncronuous:
# while True:
# try:
# with self.context['database_engine'].connect() as connection:
# connection.execute(sqlalchemy.text('PRAGMA foreign_keys=ON;'))
# #isSyncronuous = True
# break
# except sqlite3.OperationalError:
# time.sleep(0.1)
def on_mount(self) -> None: def on_mount(self) -> None:
self.push_screen(ShowsScreen()) self.push_screen(ShowsScreen())

View File

@@ -1,4 +1,15 @@
import os, re, click import os, re, click, json
from .media_descriptor import MediaDescriptor
from .pattern_controller import PatternController
#from .track_type import TrackType
#from .audio_layout import AudioLayout
#from .track_disposition import TrackDisposition
from .process import executeProcess
class FileProperties(): class FileProperties():
@@ -7,15 +18,18 @@ class FileProperties():
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)' SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)'
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)' EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
def ___init__(self, sourcePath, ):
def __init__(self, context, sourcePath):
# Separate basedir, basename and extension for current source file # Separate basedir, basename and extension for current source file
self.__sourceDirectory = os.path.dirname(sourcePath) self.__sourcePath = sourcePath
self.__sourceFilename = os.path.basename(sourcePath)
self.__sourceDirectory = os.path.dirname(self.__sourcePath)
self.__sourceFilename = os.path.basename(self.__sourcePath)
sourcePathTokens = self.__sourceFilename.split('.') sourcePathTokens = self.__sourceFilename.split('.')
if sourcePathTokens[-1] in FilenameController.FILE_EXTENSIONS: if sourcePathTokens[-1] in FileProperties.FILE_EXTENSIONS:
self.__sourceFileBasename = '.'.join(sourcePathTokens[:-1]) self.__sourceFileBasename = '.'.join(sourcePathTokens[:-1])
self.__sourceFilenameExtension = sourcePathTokens[-1] self.__sourceFilenameExtension = sourcePathTokens[-1]
else: else:
@@ -23,8 +37,8 @@ class FileProperties():
self.__sourceFilenameExtension = '' self.__sourceFilenameExtension = ''
se_match = re.compile(FilenameController.SEASON_EPISODE_INDICATOR_MATCH) se_match = re.compile(FileProperties.SEASON_EPISODE_INDICATOR_MATCH)
e_match = re.compile(FilenameController.EPISODE_INDICATOR_MATCH) e_match = re.compile(FileProperties.EPISODE_INDICATOR_MATCH)
se_result = se_match.search(self.__sourceFilename) se_result = se_match.search(self.__sourceFilename)
e_result = e_match.search(self.__sourceFilename) e_result = e_match.search(self.__sourceFilename)
@@ -42,25 +56,165 @@ class FileProperties():
file_index += 1 file_index += 1
pc = PatternController(context)
pattern = pc.matchFilename(self.__sourceFilename)
matchingFileSubtitleDescriptors = sorted([d for d in availableFileSubtitleDescriptors if d['season'] == season and d['episode'] == episode], key=lambda d: d['stream']) if availableFileSubtitleDescriptors else [] click.echo(pattern)
# 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}") #
# 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]
#
# Assemble target filename tokens def getFormatData(self):
targetFilenameTokens = [] """
targetFilenameExtension = DEFAULT_FILE_EXTENSION "format": {
"filename": "Downloads/nagatoro_s02/nagatoro_s01e02.mkv",
"nb_streams": 18,
"nb_programs": 0,
"nb_stream_groups": 0,
"format_name": "matroska,webm",
"format_long_name": "Matroska / WebM",
"start_time": "0.000000",
"duration": "1420.063000",
"size": "1489169824",
"bit_rate": "8389316",
"probe_score": 100,
"tags": {
"PUBLISHER": "Crunchyroll",
"ENCODER": "Lavf58.29.100"
}
}
"""
if label: ffprobeOutput, ffprobeError, returnCode = executeProcess(["ffprobe",
targetFilenameTokens = [label] "-hide_banner",
"-show_format",
"-of", "json",
self.__sourcePath])
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: if 'Invalid data found when processing input' in ffprobeError:
targetFilenameTokens = [sourceFileBasename] raise Exception(f"File {self.__sourcePath} does not contain valid stream data")
if returnCode != 0:
raise Exception(f"ffprobe returned with error {returnCode}")
return json.loads(ffprobeOutput)['format']
#[{'index': 0, 'codec_name': 'vp9', 'codec_long_name': 'Google VP9', 'profile': 'Profile 0', 'codec_type': 'video', 'codec_tag_string': '[0][0][0][0]', 'codec_tag': '0x0000', 'width': 1920, 'height': 1080, 'coded_width': 1920, 'coded_height': 1080, 'closed_captions': 0, 'film_grain': 0, 'has_b_frames': 0, 'sample_aspect_ratio': '1:1', 'display_aspect_ratio': '16:9', 'pix_fmt': 'yuv420p', 'level': -99, 'color_range': 'tv', 'chroma_location': 'left', 'field_order': 'progressive', 'refs': 1, 'r_frame_rate': '24000/1001', 'avg_frame_rate': '24000/1001', 'time_base': '1/1000', 'start_pts': 0, 'start_time': '0.000000', 'disposition': {'default': 1, 'dub': 0, 'original': 0, 'comment': 0, 'lyrics': 0, 'karaoke': 0, 'forced': 0, 'hearing_impaired': 0, 'visual_impaired': 0, 'clean_effects': 0, 'attached_pic': 0, 'timed_thumbnails': 0, 'non_diegetic': 0, 'captions': 0, 'descriptions': 0, 'metadata': 0, 'dependent': 0, 'still_image': 0}, 'tags': {'BPS': '7974017', 'NUMBER_OF_FRAMES': '34382', 'NUMBER_OF_BYTES': '1429358655', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 libvpx-vp9', 'DURATION': '00:23:54.016000000'}}]
#[{'index': 1, 'codec_name': 'opus', 'codec_long_name': 'Opus (Opus Interactive Audio Codec)', 'codec_type': 'audio', 'codec_tag_string': '[0][0][0][0]', 'codec_tag': '0x0000', 'sample_fmt': 'fltp', 'sample_rate': '48000', 'channels': 2, 'channel_layout': 'stereo', 'bits_per_sample': 0, 'initial_padding': 312, 'r_frame_rate': '0/0', 'avg_frame_rate': '0/0', 'time_base': '1/1000', 'start_pts': -7, 'start_time': '-0.007000', 'extradata_size': 19, 'disposition': {'default': 1, 'dub': 0, 'original': 0, 'comment': 0, 'lyrics': 0, 'karaoke': 0, 'forced': 0, 'hearing_impaired': 0, 'visual_impaired': 0, 'clean_effects': 0, 'attached_pic': 0, 'timed_thumbnails': 0, 'non_diegetic': 0, 'captions': 0, 'descriptions': 0, 'metadata': 0, 'dependent': 0, 'still_image': 0}, 'tags': {'language': 'jpn', 'title': 'Japanisch', 'BPS': '128000', 'NUMBER_OF_FRAMES': '61763', 'NUMBER_OF_BYTES': '22946145', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 libopus', 'DURATION': '00:23:54.141000000'}}]
#[{'index': 2, 'codec_name': 'webvtt', 'codec_long_name': 'WebVTT subtitle', 'codec_type': 'subtitle', 'codec_tag_string': '[0][0][0][0]', 'codec_tag': '0x0000', 'r_frame_rate': '0/0', 'avg_frame_rate': '0/0', 'time_base': '1/1000', 'start_pts': -7, 'start_time': '-0.007000', 'duration_ts': 1434141, 'duration': '1434.141000', 'disposition': {'default': 1, 'dub': 0, 'original': 0, 'comment': 0, 'lyrics': 0, 'karaoke': 0, 'forced': 0, 'hearing_impaired': 0, 'visual_impaired': 0, 'clean_effects': 0, 'attached_pic': 0, 'timed_thumbnails': 0, 'non_diegetic': 0, 'captions': 0, 'descriptions': 0, 'metadata': 0, 'dependent': 0, 'still_image': 0}, 'tags': {'language': 'ger', 'title': 'Deutsch [Full]', 'BPS': '118', 'NUMBER_OF_FRAMES': '300', 'NUMBER_OF_BYTES': '21128', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 webvtt', 'DURATION': '00:23:54.010000000'}}, {'index': 3, 'codec_name': 'webvtt', 'codec_long_name': 'WebVTT subtitle', 'codec_type': 'subtitle', 'codec_tag_string': '[0][0][0][0]', 'codec_tag': '0x0000', 'r_frame_rate': '0/0', 'avg_frame_rate': '0/0', 'time_base': '1/1000', 'start_pts': -7, 'start_time': '-0.007000', 'duration_ts': 1434141, 'duration': '1434.141000', 'disposition': {'default': 0, 'dub': 0, 'original': 0, 'comment': 0, 'lyrics': 0, 'karaoke': 0, 'forced': 0, 'hearing_impaired': 0, 'visual_impaired': 0, 'clean_effects': 0, 'attached_pic': 0, 'timed_thumbnails': 0, 'non_diegetic': 0, 'captions': 0, 'descriptions': 0, 'metadata': 0, 'dependent': 0, 'still_image': 0}, 'tags': {'language': 'eng', 'title': 'Englisch [Full]', 'BPS': '101', 'NUMBER_OF_FRAMES': '276', 'NUMBER_OF_BYTES': '16980', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 webvtt', 'DURATION': '00:23:53.230000000'}}]
def getStreamData(self):
"""Returns ffprobe stream data as array with elements according to the following example
{
"index": 4,
"codec_name": "hdmv_pgs_subtitle",
"codec_long_name": "HDMV Presentation Graphic Stream subtitles",
"codec_type": "subtitle",
"codec_tag_string": "[0][0][0][0]",
"codec_tag": "0x0000",
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/1000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 1421035,
"duration": "1421.035000",
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0,
"non_diegetic": 0,
"captions": 0,
"descriptions": 0,
"metadata": 0,
"dependent": 0,
"still_image": 0
},
"tags": {
"language": "ger",
"title": "German Full"
}
}
"""
ffprobeOutput, ffprobeError, returnCode = executeProcess(["ffprobe",
"-hide_banner",
"-show_streams",
"-of", "json",
self.__sourcePath])
if 'Invalid data found when processing input' in ffprobeError:
raise Exception(f"File {self.__sourcePath} does not contain valid stream data")
if returnCode != 0:
raise Exception(f"ffprobe returned with error {returnCode}")
return json.loads(ffprobeOutput)['streams']
# def getTrackDescriptor(self, streamObj):
# """Convert the stream describing json object into a track descriptor"""
#
# trackType = streamObj['codec_type']
#
# descriptor = {}
#
# if trackType in [t.label() for t in TrackType]:
#
# descriptor['type'] = trackType
#
# descriptor = {}
# descriptor['disposition_list'] = [t for d in (k for (k,v) in streamObj['disposition'].items() if v) if (t := TrackDisposition.find(d)) if t is not None]
#
# descriptor['tags'] = streamObj['tags'] if 'tags' in streamObj.keys() else {}
#
# if trackType == TrackType.AUDIO.label():
# descriptor['layout'] = AudioLayout.identify(streamObj)
#
# return descriptor
def getMediaDescriptor(self):
return MediaDescriptor.fromFfprobe(self.getFormatData(), self.getStreamData())
# formatData = self.getFormatData()
# streamData = self.getStreamData()

View File

@@ -73,6 +73,8 @@ class IsoLanguage(Enum):
VIETNAMESE = {"name": "Vietnamese", "iso639_1": "vi", "iso639_2": "vie"} VIETNAMESE = {"name": "Vietnamese", "iso639_1": "vi", "iso639_2": "vie"}
WELSH = {"name": "Welsh", "iso639_1": "cy", "iso639_2": "wel"} WELSH = {"name": "Welsh", "iso639_1": "cy", "iso639_2": "wel"}
UNDEFINED = {"name": "undefined", "iso639_1": "xx", "iso639_2": "und"}
@staticmethod @staticmethod
def find(label : str): def find(label : str):
@@ -81,14 +83,14 @@ class IsoLanguage(Enum):
if closestMatches: if closestMatches:
foundLangs = [l for l in IsoLanguage if l.value['name'] == closestMatches[0]] foundLangs = [l for l in IsoLanguage if l.value['name'] == closestMatches[0]]
return foundLangs[0] if foundLangs else None return foundLangs[0] if foundLangs else IsoLanguage.UNDEFINED
else: else:
return None return IsoLanguage.UNDEFINED
@staticmethod @staticmethod
def findThreeLetter(theeLetter : str): def findThreeLetter(theeLetter : str):
foundLangs = [l for l in IsoLanguage if l.value['iso639_2'] == str(theeLetter)] foundLangs = [l for l in IsoLanguage if l.value['iso639_2'] == str(theeLetter)]
return foundLangs[0] if foundLangs else None return foundLangs[0] if foundLangs else IsoLanguage.UNDEFINED
# def get(lang : str): # def get(lang : str):

View File

@@ -1,2 +1,43 @@
from ffx.track_type import TrackType
from ffx.track_descriptor import TrackDescriptor
class MediaDescriptor(): class MediaDescriptor():
pass """This class represents the structural content of a media file including streams and metadata"""
def __init__(self, **kwargs):
self.__mediaTags = kwargs['tags'] if 'tags' in kwargs.keys() else {}
self.__trackDescriptors = kwargs['trackDescriptors'] if 'trackDescriptors' in kwargs.keys() else {}
self.__clearTags = kwargs['clearTags'] if 'clearTags' in kwargs.keys() else False
@classmethod
def fromFfprobe(cls, formatData, streamData):
descriptors = {}
for streamObj in streamData:
trackType = TrackType.fromLabel(streamObj['codec_type'])
if trackType != TrackType.UNKNOWN:
if trackType.label() not in descriptors.keys():
descriptors[trackType.label()] = []
descriptors[trackType.label()].append(TrackDescriptor.fromFfprobe(streamObj))
return cls(tags=formatData['tags'] if 'tags' in formatData.keys() else {},
trackDescriptors = descriptors)
def getTags(self):
return self.__mediaTags
def getAudioTracks(self):
return self.__trackDescriptors[TrackType.AUDIO.label()] if TrackType.AUDIO.label() in self.__trackDescriptors.keys() else []
def getSubtitleTracks(self):
return self.__trackDescriptors[TrackType.SUBTITLE.label()] if TrackType.SUBTITLE.label() in self.__trackDescriptors.keys() else []

View File

@@ -0,0 +1,28 @@
# from typing import List
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey, Enum
from sqlalchemy.orm import relationship, declarative_base, sessionmaker
from .show import Base
class MediaTag(Base):
"""
relationship(argument, opt1, opt2, ...)
argument is string of class or Mapped class of the target entity
backref creates a bi-directional corresponding relationship (back_populates preferred)
back_populates points to the corresponding relationship (the actual class attribute identifier)
See: https://docs.sqlalchemy.org/en/(14|20)/orm/basic_relationships.html
"""
__tablename__ = 'media_tags'
# v1.x
id = Column(Integer, primary_key=True)
key = Column(String)
value = Column(String)
# v1.x
pattern_id = Column(Integer, ForeignKey('patterns.id', ondelete="CASCADE"))
pattern = relationship('Pattern', back_populates='media_tags')

View File

@@ -4,6 +4,7 @@ from sqlalchemy.orm import relationship, sessionmaker, Mapped, backref
from .show import Base from .show import Base
from .track import Track from .track import Track
from ffx.media_descriptor import MediaDescriptor
class Pattern(Base): class Pattern(Base):
@@ -26,3 +27,50 @@ class Pattern(Base):
# show: Mapped["Show"] = relationship(back_populates="patterns") # show: Mapped["Show"] = relationship(back_populates="patterns")
tracks = relationship('Track', back_populates='pattern', cascade="all, delete") tracks = relationship('Track', back_populates='pattern', cascade="all, delete")
media_tags = relationship('MediaTag', back_populates='pattern', cascade="all, delete")
# def getDescriptor(self):
#
# descriptor = {}
# descriptor['id'] = int(self.id)
# descriptor['pattern'] = str(self.pattern)
# descriptor['show_id'] = int(self.show_id)
#
# descriptor['tags'] = {}
# for t in self.media_tags:
# descriptor['tags'][str(t.key)] = str(t.value)
#
# return descriptor
def getShow(self):
pass
def getTracks(self):
pass
def getMediaDescriptor(self):
md = MediaDescriptor(tags = self.getDescriptor()['tags'])
for t in self.tracks:
md.appendTrack(t.getDescriptor())
return md
def getId(self):
return int(self.id)
def getPattern(self):
return str(self.pattern)
def getShowId(self):
return int(self.show_id)
def getTags(self):
return {str(k.value):str(v.value) for (k,v) in self.media_tags}

View File

@@ -39,3 +39,19 @@ class Show(Base):
index_episode_digits = Column(Integer, default=2) index_episode_digits = Column(Integer, default=2)
indicator_season_digits = Column(Integer, default=2) indicator_season_digits = Column(Integer, default=2)
indicator_episode_digits = Column(Integer, default=2) indicator_episode_digits = Column(Integer, default=2)
def getDesciptor(self):
descriptor = {}
descriptor['id'] = int(self.id)
descriptor['name'] = str(self.name)
descriptor['year'] = int(self.year)
descriptor['index_season_digits'] = int(self.index_season_digits)
descriptor['index_episode_digits'] = int(self.index_episode_digits)
descriptor['indicator_season_digits'] = int(self.indicator_season_digits)
descriptor['indicator_episode_digits'] = int(self.indicator_episode_digits)
return descriptor

View File

@@ -8,9 +8,10 @@ from ffx.track_type import TrackType
from ffx.iso_language import IsoLanguage from ffx.iso_language import IsoLanguage
from ffx.model.tag import Tag from ffx.track_disposition import TrackDisposition
from ffx.track_descriptor import TrackDescriptor
import click
class Track(Base): class Track(Base):
""" """
@@ -29,6 +30,8 @@ class Track(Base):
# P=pattern_id+sub_index+track_type # P=pattern_id+sub_index+track_type
track_type = Column(Integer) # TrackType track_type = Column(Integer) # TrackType
index = Column(Integer)
sub_index = Column(Integer) sub_index = Column(Integer)
# v1.x # v1.x
@@ -36,14 +39,135 @@ class Track(Base):
pattern = relationship('Pattern', back_populates='tracks') pattern = relationship('Pattern', back_populates='tracks')
language = Column(String) # IsoLanguage threeLetter # language = Column(String) # IsoLanguage threeLetter
title = Column(String) # title = Column(String)
tags = relationship('Tag', back_populates='track', cascade="all, delete")
track_tags = relationship('TrackTag', back_populates='track', cascade="all, delete", lazy="joined")
disposition_flags = Column(Integer) disposition_flags = Column(Integer)
def getDescriptor(self): def __init__(self, **kwargs):
pass
trackType = kwargs.pop('track_type', None)
if trackType is not None:
self.track_type = int(trackType)
# language = kwargs.pop('language', None)
# if language is not None:
# self.language = str(language.threeLetter())
dispositionSet = kwargs.pop(TrackDescriptor.DISPOSITION_SET_KEY, set())
self.disposition_flags = int(TrackDisposition.toFlags(dispositionSet))
super().__init__(**kwargs)
@classmethod
def fromStreamObj(cls, streamObj, subIndex, patternId):
"""{
'index': 4,
'codec_name': 'hdmv_pgs_subtitle',
'codec_long_name': 'HDMV Presentation Graphic Stream subtitles',
'codec_type': 'subtitle',
'codec_tag_string': '[0][0][0][0]',
'codec_tag': '0x0000',
'r_frame_rate': '0/0',
'avg_frame_rate': '0/0',
'time_base': '1/1000',
'start_pts': 0,
'start_time': '0.000000',
'duration_ts': 1421035,
'duration': '1421.035000',
'disposition': {
'default': 1,
'dub': 0,
'original': 0,
'comment': 0,
'lyrics': 0,
'karaoke': 0,
'forced': 0,
'hearing_impaired': 0,
'visual_impaired': 0,
'clean_effects': 0,
'attached_pic': 0,
'timed_thumbnails': 0,
'non_diegetic': 0,
'captions': 0,
'descriptions': 0,
'metadata': 0,
'dependent': 0,
'still_image': 0
},
'tags': {
'language': 'ger',
'title': 'German Full'
}
}
# v1.x
id = Column(Integer, primary_key=True, autoincrement = True)
# P=pattern_id+sub_index+track_type
track_type = Column(Integer) # TrackType
sub_index = Column(Integer)
# v1.x
pattern_id = Column(Integer, ForeignKey('patterns.id', ondelete='CASCADE'))
pattern = relationship('Pattern', back_populates='tracks')
language = Column(String) # IsoLanguage threeLetter
title = Column(String)
track_tags = relationship('TrackTag', back_populates='track', cascade='all, delete')
disposition_flags = Column(Integer)
"""
trackType = streamObj['codec_type']
if trackType in [t.label() for t in TrackType]:
return cls(pattern_id = patternId,
sub_index = int(subIndex),
track_type = trackType,
disposition_flags = sum([2**t.index() for (k,v) in streamObj['disposition'].items() if v and (t := TrackDisposition.find(k)) is not None]))
else:
return None
def getId(self):
return int(self.id)
def getPatternId(self):
return int(self.pattern_id)
def getType(self):
return TrackType.fromIndex(self.track_type)
def getSubIndex(self):
return int(self.sub_index)
def getLanguage(self):
tags = {t.key:t.value for t in self.track_tags}
return IsoLanguage.findThreeLetter(tags['language']) if 'language' in tags.keys() else IsoLanguage.UNDEFINED
def getTitle(self):
tags = {t.key:t.value for t in self.track_tags}
return tags['title'] if 'title' in tags.keys() else ''
def getDispositionSet(self):
return TrackDisposition.toSet(self.disposition_flags)
def getTags(self):
return {str(t.key):str(t.value) for t in self.track_tags}

View File

@@ -4,10 +4,8 @@ from sqlalchemy.orm import relationship, declarative_base, sessionmaker
from .show import Base from .show import Base
from ffx.track_type import TrackType
class TrackTag(Base):
class Tag(Base):
""" """
relationship(argument, opt1, opt2, ...) relationship(argument, opt1, opt2, ...)
argument is string of class or Mapped class of the target entity argument is string of class or Mapped class of the target entity
@@ -17,7 +15,7 @@ class Tag(Base):
See: https://docs.sqlalchemy.org/en/(14|20)/orm/basic_relationships.html See: https://docs.sqlalchemy.org/en/(14|20)/orm/basic_relationships.html
""" """
__tablename__ = 'tags' __tablename__ = 'track_tags'
# v1.x # v1.x
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
@@ -27,4 +25,4 @@ class Tag(Base):
# v1.x # v1.x
track_id = Column(Integer, ForeignKey('tracks.id', ondelete="CASCADE")) track_id = Column(Integer, ForeignKey('tracks.id', ondelete="CASCADE"))
track = relationship('Track', back_populates='tags') track = relationship('Track', back_populates='track_tags')

View File

@@ -2,13 +2,15 @@ import click, re
from ffx.model.pattern import Pattern from ffx.model.pattern import Pattern
from .media_descriptor import MediaDescriptor
class PatternController(): class PatternController():
def __init__(self, context): def __init__(self, context):
self.context = context self.context = context
self.Session = self.context['database_session'] # convenience self.Session = self.context['database']['session'] # convenience
def addPattern(self, patternDescriptor): def addPattern(self, patternDescriptor):
@@ -77,26 +79,24 @@ class PatternController():
s.close() s.close()
def getPatternDict(self, pattern): def getPattern(self, patternId : int):
patternDescriptor = {}
patternDescriptor['id'] = int(pattern.id)
patternDescriptor['pattern'] = str(pattern.pattern)
patternDescriptor['show_id'] = int(pattern.show_id)
return patternDescriptor
if type(patternId) is not int:
def getPatternDescriptor(self, patternId): raise ValueError(f"PatternController.getPattern(): Argument patternId is required to be of type int")
try: try:
s = self.Session() s = self.Session()
q = s.query(Pattern).filter(Pattern.id == int(patternId)) q = s.query(Pattern).filter(Pattern.id == int(patternId))
if q.count(): if q.count():
pattern = q.first() # pattern = q.first()
return self.getPatternDict(pattern) #return self.getPatternDict(pattern)
return q.first()
else:
return None
except Exception as ex: except Exception as ex:
raise click.ClickException(f"PatternController.getPatternDescriptor(): {repr(ex)}") raise click.ClickException(f"PatternController.getPattern(): {repr(ex)}")
finally: finally:
s.close() s.close()
@@ -125,39 +125,62 @@ class PatternController():
def matchFilename(self, filename): def matchFilename(self, filename):
SEASON_PATTERN = '[sS]([0-9]+)' #SEASON_PATTERN = '[sS]([0-9]+)'
EPISODE_PATTERN = '[eE]([0-9]+)' #EPISODE_PATTERN = '[eE]([0-9]+)'
result = {} #result = {}
try: try:
s = self.Session() s = self.Session()
q = s.query(Pattern) q = s.query(Pattern)
for pattern in q.all(): matchedPatterns = [p for p in q.all() if re.search(p.pattern, filename)]
match = re.search(pattern.pattern, filename) if matchedPatterns:
return matchedPatterns[0]
else:
return None
if match: # for pattern in q.all():
#
result['pattern_id'] = pattern.id # match = re.search(pattern.pattern, filename)
result['show_id'] = pattern.show_id #
# if match:
result['indicator'] = match.group(1) #
# result['pattern_id'] = pattern.id
seasonMatch = re.search(SEASON_PATTERN, result['indicator']) # result['show_id'] = pattern.show_id
if seasonMatch: #
result['season'] = int(seasonMatch.group(1)) # result['indicator'] = match.group(1)
#
episodeMatch = re.search(EPISODE_PATTERN, result['indicator']) # seasonMatch = re.search(SEASON_PATTERN, result['indicator'])
if episodeMatch: # if seasonMatch:
result['episode'] = int(episodeMatch.group(1)) # result['season'] = int(seasonMatch.group(1))
#
# episodeMatch = re.search(EPISODE_PATTERN, result['indicator'])
# if episodeMatch:
# result['episode'] = int(episodeMatch.group(1))
except Exception as ex: except Exception as ex:
raise click.ClickException(f"PatternController.matchFilename(): {repr(ex)}") raise click.ClickException(f"PatternController.findPattern(): {repr(ex)}")
finally: finally:
s.close() s.close()
return result return result
def getMediaDescriptor(self, patternId):
try:
s = self.Session()
q = s.query(Pattern).filter(Pattern.id == int(patternId))
if q.count():
pattern = q.first()
#return self.getPatternDict(pattern)
return pattern.getMediaDescriptor()
except Exception as ex:
raise click.ClickException(f"PatternController.getPatternDescriptor(): {repr(ex)}")
finally:
s.close()

View File

@@ -50,7 +50,7 @@ class PatternDeleteScreen(Screen):
super().__init__() super().__init__()
self.context = self.app.getContext() self.context = self.app.getContext()
self.Session = self.context['database_session'] # convenience self.Session = self.context['database']['session'] # convenience
self.__pc = PatternController(context = self.context) self.__pc = PatternController(context = self.context)
self.__sc = ShowController(context = self.context) self.__sc = ShowController(context = self.context)

View File

@@ -19,6 +19,7 @@ from .track_delete_screen import TrackDeleteScreen
from ffx.track_type import TrackType from ffx.track_type import TrackType
from ffx.track_disposition import TrackDisposition from ffx.track_disposition import TrackDisposition
from ffx.track_descriptor import TrackDescriptor
from textual.widgets._data_table import CellDoesNotExist from textual.widgets._data_table import CellDoesNotExist
@@ -73,13 +74,13 @@ class PatternDetailsScreen(Screen):
super().__init__() super().__init__()
self.context = self.app.getContext() self.context = self.app.getContext()
self.Session = self.context['database_session'] # convenience self.Session = self.context['database']['session'] # convenience
self.__pc = PatternController(context = self.context) self.__pc = PatternController(context = self.context)
self.__sc = ShowController(context = self.context) self.__sc = ShowController(context = self.context)
self.__tc = TrackController(context = self.context) self.__tc = TrackController(context = self.context)
self.pattern_obj = self.__pc.getPatternDescriptor(patternId) if patternId is not None else {} self.__pattern = self.__pc.getPattern(patternId) if patternId is not None else None
self.show_obj = self.__sc.getShowDesciptor(showId) if showId is not None else {} self.show_obj = self.__sc.getShowDesciptor(showId) if showId is not None else {}
@@ -98,7 +99,7 @@ class PatternDetailsScreen(Screen):
return [{'id': int(p.id), 'pattern': p.pattern} for p in q.all()] return [{'id': int(p.id), 'pattern': p.pattern} for p in q.all()]
except Exception as ex: except Exception as ex:
click.ClickException(f"loadPatterns(): {repr(ex)}") raise click.ClickException(f"loadTracks(): {repr(ex)}")
finally: finally:
s.close() s.close()
@@ -107,53 +108,53 @@ class PatternDetailsScreen(Screen):
self.audioStreamsTable.clear() self.audioStreamsTable.clear()
trackIds = self.__tc.findAllTracks(self.pattern_obj['id']) if self.__pattern is not None:
for audioTrackId in trackIds[TrackType.AUDIO.label()]: audioTracks = self.__tc.findAudioTracks(self.__pattern.getId())
ad = self.__tc.getTrackDescriptor(audioTrackId) for at in audioTracks:
dispoList = ad['disposition_list']
row = (ad['sub_index'], dispoSet = at.getDispositionSet()
" ",
ad['language'].label(),
ad['title'],
'Yes' if TrackDisposition.DEFAULT in dispoList else 'No',
'Yes' if TrackDisposition.FORCED in dispoList else 'No')
self.audioStreamsTable.add_row(*map(str, row)) row = (at.getSubIndex(),
" ",
at.getLanguage().label(),
at.getTitle(),
'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
self.audioStreamsTable.add_row(*map(str, row))
def updateSubtitleTracks(self): def updateSubtitleTracks(self):
self.subtitleStreamsTable.clear() self.subtitleStreamsTable.clear()
trackIds = self.__tc.findAllTracks(self.pattern_obj['id']) if self.__pattern is not None:
for subtitleTrackId in trackIds[TrackType.SUBTITLE.label()]: subtitleTracks = self.__tc.findSubtitleTracks(self.__pattern.getId())
sd = self.__tc.getTrackDescriptor(subtitleTrackId) for st in subtitleTracks:
dispoList = sd['disposition_list']
row = (sd['sub_index'], dispoSet = st.getDispositionSet()
" ",
sd['language'].label(),
sd['title'],
'Yes' if TrackDisposition.DEFAULT in dispoList else 'No',
'Yes' if TrackDisposition.FORCED in dispoList else 'No')
self.subtitleStreamsTable.add_row(*map(str, row)) row = (st.getSubIndex(),
" ",
st.getLanguage().label(),
st.getTitle(),
'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
self.subtitleStreamsTable.add_row(*map(str, row))
def on_mount(self): def on_mount(self):
if self.pattern_obj:
self.query_one("#pattern_input", Input).value = str(self.pattern_obj['pattern'])
if self.show_obj: if self.show_obj:
self.query_one("#showlabel", Static).update(f"{self.show_obj['id']} - {self.show_obj['name']} ({self.show_obj['year']})") self.query_one("#showlabel", Static).update(f"{self.show_obj['id']} - {self.show_obj['name']} ({self.show_obj['year']})")
if self.__pattern is not None:
if self.pattern_obj: self.query_one("#pattern_input", Input).value = str(self.__pattern.getPattern())
self.updateAudioTracks() self.updateAudioTracks()
self.updateSubtitleTracks() self.updateSubtitleTracks()
@@ -192,7 +193,7 @@ class PatternDetailsScreen(Screen):
with Grid(): with Grid():
# 1 # 1
yield Static("Edit filename pattern" if self.pattern_obj else "New filename pattern", id="toplabel") yield Static("Edit filename pattern" if self.__pattern is not None else "New filename pattern", id="toplabel")
yield Input(type="text", id="pattern_input", classes="four") yield Input(type="text", id="pattern_input", classes="four")
# 2 # 2
@@ -209,7 +210,7 @@ class PatternDetailsScreen(Screen):
yield Static("Audio streams") yield Static("Audio streams")
yield Static(" ") yield Static(" ")
if self.pattern_obj: if self.__pattern is not None:
yield Button("Add", id="button_add_audio_stream") yield Button("Add", id="button_add_audio_stream")
yield Button("Edit", id="button_edit_audio_stream") yield Button("Edit", id="button_edit_audio_stream")
yield Button("Delete", id="button_delete_audio_stream") yield Button("Delete", id="button_delete_audio_stream")
@@ -227,7 +228,7 @@ class PatternDetailsScreen(Screen):
yield Static("Subtitle streams") yield Static("Subtitle streams")
yield Static(" ") yield Static(" ")
if self.pattern_obj: if self.__pattern is not None:
yield Button("Add", id="button_add_subtitle_stream") yield Button("Add", id="button_add_subtitle_stream")
yield Button("Edit", id="button_edit_subtitle_stream") yield Button("Edit", id="button_edit_subtitle_stream")
yield Button("Delete", id="button_delete_subtitle_stream") yield Button("Delete", id="button_delete_subtitle_stream")
@@ -253,9 +254,9 @@ class PatternDetailsScreen(Screen):
def getSelectedAudioTrackId(self): def getSelectedAudioTrackDescriptor(self):
if not self.pattern_obj: if not self.__pattern:
return None return None
try: try:
@@ -269,7 +270,18 @@ class PatternDetailsScreen(Screen):
subIndex = int(selected_track_data[0]) subIndex = int(selected_track_data[0])
return self.__tc.findTrack(self.pattern_obj['id'], TrackType.AUDIO, subIndex) audioTrack = self.__tc.findTrack(self.__pattern.getId(), TrackType.AUDIO, subIndex)
kwargs = {}
kwargs[TrackDescriptor.PATTERN_ID_KEY] = self.__pattern.getId()
kwargs[TrackDescriptor.SUB_INDEX_KEY] = subIndex
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = TrackType.AUDIO
kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = audioTrack.getDispositionSet()
kwargs[TrackDescriptor.TAGS_KEY] = audioTrack.getTags()
return TrackDescriptor(**kwargs)
else: else:
return None return None
@@ -278,9 +290,9 @@ class PatternDetailsScreen(Screen):
return None return None
def getSelectedSubtitleTrackId(self): def getSelectedSubtitleTrackDescriptor(self):
if not self.pattern_obj: if not self.__pattern is None:
return None return None
try: try:
@@ -294,7 +306,18 @@ class PatternDetailsScreen(Screen):
subIndex = int(selected_track_data[0]) subIndex = int(selected_track_data[0])
return self.__tc.findTrack(self.pattern_obj['id'], TrackType.SUBTITLE, subIndex) subtitleTrack = self.__tc.findTrack(self.__pattern.getId(), TrackType.SUBTITLE, subIndex)
kwargs = {}
kwargs[TrackDescriptor.PATTERN_ID_KEY] = self.__pattern.getId()
kwargs[TrackDescriptor.SUB_INDEX_KEY] = subIndex
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = TrackType.SUBTITLE
kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = subtitleTrack.getDispositionSet()
kwargs[TrackDescriptor.TAGS_KEY] = subtitleTrack.getTags()
return TrackDescriptor(**kwargs)
else: else:
return None return None
@@ -313,9 +336,9 @@ class PatternDetailsScreen(Screen):
patternDescriptor['show_id'] = self.show_obj['id'] patternDescriptor['show_id'] = self.show_obj['id']
patternDescriptor['pattern'] = self.getPatternFromInput() patternDescriptor['pattern'] = self.getPatternFromInput()
if self.pattern_obj: if self.__pattern is not None:
if self.__pc.updatePattern(self.pattern_obj['id'], patternDescriptor): if self.__pc.updatePattern(self.__pattern.getId(), patternDescriptor):
self.dismiss(patternDescriptor) self.dismiss(patternDescriptor)
else: else:
#TODO: Meldung #TODO: Meldung
@@ -335,29 +358,28 @@ class PatternDetailsScreen(Screen):
# Save pattern when just created before adding streams # Save pattern when just created before adding streams
if self.pattern_obj: if self.__pattern is not None:
#self.pattern_obj
if event.button.id == "button_add_audio_stream": if event.button.id == "button_add_audio_stream":
self.app.push_screen(TrackDetailsScreen(trackType = TrackType.AUDIO, patternId = self.pattern_obj['id'], subIndex = len(self.audioStreamsTable.rows)), self.handle_add_track) self.app.push_screen(TrackDetailsScreen(trackType = TrackType.AUDIO, patternId = self.__pattern.getId(), subIndex = len(self.audioStreamsTable.rows)), self.handle_add_track)
selectedAudioTrackId = self.getSelectedAudioTrackId() selectedAudioTrack = self.getSelectedAudioTrackDescriptor()
if selectedAudioTrackId is not None: if selectedAudioTrack is not None:
if event.button.id == "button_edit_audio_stream": if event.button.id == "button_edit_audio_stream":
self.app.push_screen(TrackDetailsScreen(trackId = selectedAudioTrackId), self.handle_edit_track)
self.app.push_screen(TrackDetailsScreen(trackDescriptor = selectedAudioTrack), self.handle_edit_track)
if event.button.id == "button_delete_audio_stream": if event.button.id == "button_delete_audio_stream":
self.app.push_screen(TrackDeleteScreen(trackId = selectedAudioTrackId), self.handle_delete_track) self.app.push_screen(TrackDeleteScreen(trackDescriptor = selectedAudioTrack), self.handle_delete_track)
if event.button.id == "button_add_subtitle_stream": if event.button.id == "button_add_subtitle_stream":
self.app.push_screen(TrackDetailsScreen(trackType = TrackType.SUBTITLE, patternId = self.pattern_obj['id'], subIndex = len(self.subtitleStreamsTable.rows)), self.handle_add_track) self.app.push_screen(TrackDetailsScreen(trackType = TrackType.SUBTITLE, patternId = self.__pattern.getId(), subIndex = len(self.subtitleStreamsTable.rows)), self.handle_add_track)
selectedSubtitleTrackId = self.getSelectedSubtitleTrackId() selectedSubtitleTrack = self.getSelectedSubtitleTrackDescriptor()
if selectedSubtitleTrackId is not None: if selectedSubtitleTrack is not None:
if event.button.id == "button_edit_subtitle_stream": if event.button.id == "button_edit_subtitle_stream":
self.app.push_screen(TrackDetailsScreen(trackId = selectedSubtitleTrackId), self.handle_edit_track) self.app.push_screen(TrackDetailsScreen(trackDescriptor = selectedSubtitleTrack), self.handle_edit_track)
if event.button.id == "button_delete_subtitle_stream": if event.button.id == "button_delete_subtitle_stream":
self.app.push_screen(TrackDeleteScreen(trackId = selectedSubtitleTrackId), self.handle_delete_track) self.app.push_screen(TrackDeleteScreen(trackDescriptor = selectedSubtitleTrack), self.handle_delete_track)
if event.button.id == "patternbutton": if event.button.id == "patternbutton":
@@ -373,63 +395,67 @@ class PatternDetailsScreen(Screen):
def handle_add_track(self, trackDescriptor): def handle_add_track(self, trackDescriptor):
dispoList = trackDescriptor['disposition_list'] dispoSet = trackDescriptor.getDispositionSet()
trackType = trackDescriptor.getType()
subIndex = trackDescriptor.getSubIndex()
language = trackDescriptor.getLanguage()
title = trackDescriptor.getTitle()
if trackDescriptor['type'] == TrackType.AUDIO: if trackType == TrackType.AUDIO:
row = (trackDescriptor['sub_index'], row = (subIndex,
" ", " ",
trackDescriptor['language'].label(), language.label(),
trackDescriptor['title'], title,
'Yes' if TrackDisposition.DEFAULT in dispoList else 'No', 'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
'Yes' if TrackDisposition.FORCED in dispoList else 'No') 'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
self.audioStreamsTable.add_row(*map(str, row)) self.audioStreamsTable.add_row(*map(str, row))
if trackDescriptor['type'] == TrackType.SUBTITLE: if trackType == TrackType.SUBTITLE:
row = (trackDescriptor['sub_index'], row = (subIndex,
" ", " ",
trackDescriptor['language'].label(), language.label(),
trackDescriptor['title'], title,
'Yes' if TrackDisposition.DEFAULT in dispoList else 'No', 'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
'Yes' if TrackDisposition.FORCED in dispoList else 'No') 'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
self.subtitleStreamsTable.add_row(*map(str, row)) self.subtitleStreamsTable.add_row(*map(str, row))
def handle_edit_track(self, trackDescriptor): def handle_edit_track(self, trackDescriptor : TrackDescriptor):
try: try:
if trackDescriptor['type'] == TrackType.AUDIO: if trackDescriptor.getType() == TrackType.AUDIO:
row_key, col_key = self.audioStreamsTable.coordinate_to_cell_key(self.audioStreamsTable.cursor_coordinate) row_key, col_key = self.audioStreamsTable.coordinate_to_cell_key(self.audioStreamsTable.cursor_coordinate)
self.audioStreamsTable.update_cell(row_key, self.column_key_audio_language, trackDescriptor['language'].label()) self.audioStreamsTable.update_cell(row_key, self.column_key_audio_language, trackDescriptor.getLanguage().label())
self.audioStreamsTable.update_cell(row_key, self.column_key_audio_title, trackDescriptor['title']) self.audioStreamsTable.update_cell(row_key, self.column_key_audio_title, trackDescriptor.getTitle())
self.audioStreamsTable.update_cell(row_key, self.column_key_audio_default, 'Yes' if TrackDisposition.DEFAULT in trackDescriptor['disposition_list'] else 'No') self.audioStreamsTable.update_cell(row_key, self.column_key_audio_default, 'Yes' if TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() else 'No')
self.audioStreamsTable.update_cell(row_key, self.column_key_audio_forced, 'Yes' if TrackDisposition.FORCED in trackDescriptor['disposition_list'] else 'No') self.audioStreamsTable.update_cell(row_key, self.column_key_audio_forced, 'Yes' if TrackDisposition.FORCED in trackDescriptor.getDispositionSet() else 'No')
if trackDescriptor['type'] == TrackType.SUBTITLE: if trackDescriptor.getType() == TrackType.SUBTITLE:
row_key, col_key = self.subtitleStreamsTable.coordinate_to_cell_key(self.subtitleStreamsTable.cursor_coordinate) row_key, col_key = self.subtitleStreamsTable.coordinate_to_cell_key(self.subtitleStreamsTable.cursor_coordinate)
self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_language, trackDescriptor['language'].label()) self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_language, trackDescriptor.getLanguage().label())
self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_title, trackDescriptor['title']) self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_title, trackDescriptor.getTitle())
self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_default, 'Yes' if TrackDisposition.DEFAULT in trackDescriptor['disposition_list'] else 'No') self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_default, 'Yes' if TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() else 'No')
self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_forced, 'Yes' if TrackDisposition.FORCED in trackDescriptor['disposition_list'] else 'No') self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_forced, 'Yes' if TrackDisposition.FORCED in trackDescriptor.getDispositionSet() else 'No')
except CellDoesNotExist: except CellDoesNotExist:
pass pass
def handle_delete_track(self, trackDescriptor): def handle_delete_track(self, trackDescriptor : TrackDescriptor):
try: try:
if trackDescriptor['type'] == TrackType.AUDIO: if trackDescriptor.getType() == TrackType.AUDIO:
self.updateAudioTracks() self.updateAudioTracks()
if trackDescriptor['type'] == TrackType.SUBTITLE: if trackDescriptor.getType() == TrackType.SUBTITLE:
self.updateSubtitleTracks() self.updateSubtitleTracks()
except CellDoesNotExist: except CellDoesNotExist:

16
bin/ffx/process.py Normal file
View File

@@ -0,0 +1,16 @@
import subprocess
#class ProcessController():
#def __init__(self, commandSequence):
# self.__commandSequence = commandSequence
def executeProcess(commandSequence):
process = subprocess.Popen(commandSequence, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, error = process.communicate()
return output.decode('utf-8'), error.decode('utf-8'), process.returncode

View File

@@ -8,7 +8,7 @@ class ShowController():
def __init__(self, context): def __init__(self, context):
self.context = context self.context = context
self.Session = self.context['database_session'] # convenience self.Session = self.context['database']['session'] # convenience
def getShowDesciptor(self, showId): def getShowDesciptor(self, showId):
@@ -22,16 +22,19 @@ class ShowController():
if q.count(): if q.count():
show = q.first() show = q.first()
showDescriptor['id'] = int(show.id) # showDescriptor['id'] = int(show.id)
showDescriptor['name'] = str(show.name) # showDescriptor['name'] = str(show.name)
showDescriptor['year'] = int(show.year) # showDescriptor['year'] = int(show.year)
#
# showDescriptor['index_season_digits'] = int(show.index_season_digits)
# showDescriptor['index_episode_digits'] = int(show.index_episode_digits)
# showDescriptor['indicator_season_digits'] = int(show.indicator_season_digits)
# showDescriptor['indicator_episode_digits'] = int(show.indicator_episode_digits)
#
# return showDescriptor
showDescriptor['index_season_digits'] = int(show.index_season_digits) return show.getDesciptor()
showDescriptor['index_episode_digits'] = int(show.index_episode_digits)
showDescriptor['indicator_season_digits'] = int(show.indicator_season_digits)
showDescriptor['indicator_episode_digits'] = int(show.indicator_episode_digits)
return showDescriptor
except Exception as ex: except Exception as ex:
raise click.ClickException(f"ShowController.getShowDesciptor(): {repr(ex)}") raise click.ClickException(f"ShowController.getShowDesciptor(): {repr(ex)}")

View File

@@ -48,7 +48,7 @@ class ShowDeleteScreen(Screen):
super().__init__() super().__init__()
self.context = self.app.getContext() self.context = self.app.getContext()
self.Session = self.context['database_session'] # convenience self.Session = self.context['database']['session'] # convenience
self.__sc = ShowController(context = self.context) self.__sc = ShowController(context = self.context)
self.show_obj = self.__sc.getShowDesciptor(showId) if showId is not None else {} self.show_obj = self.__sc.getShowDesciptor(showId) if showId is not None else {}

View File

@@ -76,7 +76,7 @@ class ShowDetailsScreen(Screen):
super().__init__() super().__init__()
self.context = self.app.getContext() self.context = self.app.getContext()
self.Session = self.context['database_session'] # convenience self.Session = self.context['database']['session'] # convenience
self.__sc = ShowController(context = self.context) self.__sc = ShowController(context = self.context)
self.__pc = PatternController(context = self.context) self.__pc = PatternController(context = self.context)

View File

@@ -57,7 +57,7 @@ class ShowsScreen(Screen):
self.context = self.app.getContext() self.context = self.app.getContext()
self.Session = self.context['database_session'] # convenience self.Session = self.context['database']['session'] # convenience

193
bin/ffx/tag_controller.py Normal file
View File

@@ -0,0 +1,193 @@
import click
from ffx.model.track import Track
from .track_type import TrackType
from .track_disposition import TrackDisposition
from .iso_language import IsoLanguage
from ffx.model.media_tag import MediaTag
from ffx.model.track_tag import TrackTag
class TagController():
def __init__(self, context):
self.context = context
self.Session = self.context['database']['session'] # convenience
def addMediaTag(self, trackDescriptor):
try:
s = self.Session()
track = Track(pattern_id = int(trackDescriptor['pattern_id']),
track_type = int(trackDescriptor['type'].value),
sub_index = int(trackDescriptor['sub_index']),
# language = str(trackDescriptor['language'].threeLetter()),
# title = str(trackDescriptor['title']),
disposition_flags = int(TrackDisposition.toFlags(trackDescriptor['disposition_list'])))
s.add(track)
s.commit()
except Exception as ex:
raise click.ClickException(f"TrackController.addTrack(): {repr(ex)}")
finally:
s.close()
def addTrackTag(self, trackDescriptor):
try:
s = self.Session()
track = Track(pattern_id = int(trackDescriptor['pattern_id']),
track_type = int(trackDescriptor['type'].value),
sub_index = int(trackDescriptor['sub_index']),
# language = str(trackDescriptor['language'].threeLetter()),
# title = str(trackDescriptor['title']),
disposition_flags = int(TrackDisposition.toFlags(trackDescriptor['disposition_list'])))
s.add(track)
s.commit()
except Exception as ex:
raise click.ClickException(f"TrackController.addTrack(): {repr(ex)}")
finally:
s.close()
def updateTrack(self, trackId, trackDescriptor):
try:
s = self.Session()
q = s.query(Track).filter(Track.id == int(trackId))
if q.count():
track = q.first()
track.sub_index = int(trackDescriptor['sub_index'])
# track.language = str(trackDescriptor['language'].threeLetter())
# track.title = str(trackDescriptor['title'])
track.disposition_flags = int(TrackDisposition.toFlags(trackDescriptor['disposition_list']))
s.commit()
return True
else:
return False
except Exception as ex:
raise click.ClickException(f"TrackController.addTrack(): {repr(ex)}")
finally:
s.close()
def findAllTracks(self, patternId):
try:
s = self.Session()
trackDescriptors = {}
trackDescriptors[TrackType.AUDIO.label()] = []
trackDescriptors[TrackType.SUBTITLE.label()] = []
q_audio = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == TrackType.AUDIO.index())
for audioTrack in q_audio.all():
trackDescriptors[TrackType.AUDIO.label()].append(audioTrack.id)
q_subtitle = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == TrackType.SUBTITLE.index())
for subtitleTrack in q_subtitle.all():
trackDescriptors[TrackType.SUBTITLE.label()].append(subtitleTrack.id)
return trackDescriptors
except Exception as ex:
raise click.ClickException(f"TrackController.findAllTracks(): {repr(ex)}")
finally:
s.close()
def findTrack(self, patternId, trackType : TrackType, subIndex):
try:
s = self.Session()
q = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == trackType.value, Track.sub_index == int(subIndex))
if q.count():
track = q.first()
return int(track.id)
else:
return None
except Exception as ex:
raise click.ClickException(f"TrackController.findTrack(): {repr(ex)}")
finally:
s.close()
def getTrackDescriptor(self, trackId):
try:
s = self.Session()
q = s.query(Track).filter(Track.id == int(trackId))
if q.count():
track = q.first()
#return self.getTrackDict(track)
return track.getDescriptor()
else:
return {}
except Exception as ex:
raise click.ClickException(f"TrackController.getTrackDescriptor(): {repr(ex)}")
finally:
s.close()
def deleteTrack(self, trackId):
try:
s = self.Session()
q = s.query(Track).filter(Track.id == int(trackId))
if q.count():
trackDescriptor = self.getTrackDict(q.first())
q_siblings = s.query(Track).filter(Track.pattern_id == int(trackDescriptor['pattern_id']), Track.track_type == trackDescriptor['type'].value).order_by(Track.sub_index)
subIndex = 0
for track in q_siblings.all():
if track.sub_index == trackDescriptor['sub_index']:
s.delete(track)
else:
track.sub_index = subIndex
subIndex += 1
s.commit()
return True
return False
except Exception as ex:
raise click.ClickException(f"TrackController.deleteTrack(): {repr(ex)}")
finally:
s.close()

View File

@@ -9,13 +9,15 @@ from .iso_language import IsoLanguage
from .track_type import TrackType from .track_type import TrackType
from ffx.model.track_tag import TrackTag
class TrackController(): class TrackController():
def __init__(self, context): def __init__(self, context):
self.context = context self.context = context
self.Session = self.context['database_session'] # convenience self.Session = self.context['database']['session'] # convenience
def addTrack(self, trackDescriptor): def addTrack(self, trackDescriptor):
@@ -23,21 +25,22 @@ class TrackController():
try: try:
s = self.Session() s = self.Session()
track = Track(pattern_id = int(trackDescriptor['pattern_id']), track = Track(pattern_id = int(trackDescriptor.getPatternId()),
track_type = int(trackDescriptor.getType().index()),
track_type = int(trackDescriptor['type'].value), sub_index = int(trackDescriptor.getSubIndex()),
disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet())))
sub_index = int(trackDescriptor['sub_index']),
language = str(trackDescriptor['language'].threeLetter()),
title = str(trackDescriptor['title']),
disposition_flags = int(TrackDisposition.toFlags(trackDescriptor['disposition_list'])))
s.add(track) s.add(track)
s.commit() s.commit()
for k,v in trackDescriptor.getTags().items():
tag = TrackTag(track_id = track.id,
key = k,
value = v)
s.add(tag)
s.commit()
except Exception as ex: except Exception as ex:
raise click.ClickException(f"TrackController.addTrack(): {repr(ex)}") raise click.ClickException(f"TrackController.addTrack(): {repr(ex)}")
finally: finally:
@@ -55,9 +58,9 @@ class TrackController():
track = q.first() track = q.first()
track.sub_index = int(trackDescriptor['sub_index']) track.sub_index = int(trackDescriptor['sub_index'])
track.language = str(trackDescriptor['language'].threeLetter()) # track.language = str(trackDescriptor['language'].threeLetter())
track.title = str(trackDescriptor['title']) # track.title = str(trackDescriptor['title'])
track.disposition_flags = int(TrackDisposition.toFlags(trackDescriptor['disposition_list'])) track.disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet()))
s.commit() s.commit()
@@ -71,44 +74,68 @@ class TrackController():
finally: finally:
s.close() s.close()
#
def findAllTracks(self, patternId): # def findAllTracks(self, patternId):
#
# try:
# s = self.Session()
#
# trackDescriptors = {}
# trackDescriptors[TrackType.AUDIO.label()] = []
# trackDescriptors[TrackType.SUBTITLE.label()] = []
#
# q_audio = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == TrackType.AUDIO.index())
# for audioTrack in q_audio.all():
# trackDescriptors[TrackType.AUDIO.label()].append(audioTrack.id)
#
# q_subtitle = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == TrackType.SUBTITLE.index())
# for subtitleTrack in q_subtitle.all():
# trackDescriptors[TrackType.SUBTITLE.label()].append(subtitleTrack.id)
#
# return trackDescriptors
#
# except Exception as ex:
# raise click.ClickException(f"TrackController.findAllTracks(): {repr(ex)}")
# finally:
# s.close()
#
def findAudioTracks(self, patternId):
try: try:
s = self.Session() s = self.Session()
trackDescriptors = {} q = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == TrackType.AUDIO.index())
trackDescriptors[TrackType.AUDIO.label()] = [] return [a for a in q.all()]
trackDescriptors[TrackType.SUBTITLE.label()] = []
q_audio = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == TrackType.AUDIO.value)
for audioTrack in q_audio.all():
trackDescriptors[TrackType.AUDIO.label()].append(audioTrack.id)
q_subtitle = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == TrackType.SUBTITLE.value)
for subtitleTrack in q_subtitle.all():
trackDescriptors[TrackType.SUBTITLE.label()].append(subtitleTrack.id)
return trackDescriptors
except Exception as ex: except Exception as ex:
raise click.ClickException(f"TrackController.findAllTracks(): {repr(ex)}") raise click.ClickException(f"TrackController.findAudioTracks(): {repr(ex)}")
finally:
s.close()
def findSubtitleTracks(self, patternId):
try:
s = self.Session()
q = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == TrackType.SUBTITLE.index())
return [s for s in q.all()]
except Exception as ex:
raise click.ClickException(f"TrackController.findSubtitleTracks(): {repr(ex)}")
finally: finally:
s.close() s.close()
def findTrack(self, patternId, trackType : TrackType, subIndex): def findTrack(self, patternId : int, trackType : TrackType, subIndex : int):
try: try:
s = self.Session() s = self.Session()
q = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == trackType.value, Track.sub_index == int(subIndex)) q = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == trackType.index(), Track.sub_index == int(subIndex))
if q.count(): if q.count():
track = q.first() #track = q.first()
return int(track.id) #return int(track.id)
return q.first()
else: else:
return None return None
@@ -117,37 +144,6 @@ class TrackController():
finally: finally:
s.close() s.close()
def getTrackDict(self, track):
trackDescriptor = {}
trackDescriptor['id'] = int(track.id)
trackDescriptor['pattern_id'] = int(track.pattern_id)
trackDescriptor['type'] = TrackType(track.track_type)
trackDescriptor['sub_index'] = int(track.sub_index)
trackDescriptor['language'] = IsoLanguage.findThreeLetter(track.language)
trackDescriptor['title'] = str(track.title)
trackDescriptor['disposition_list'] = TrackDisposition.toList(track.disposition_flags)
return trackDescriptor
def getTrackDescriptor(self, trackId):
try:
s = self.Session()
q = s.query(Track).filter(Track.id == int(trackId))
if q.count():
track = q.first()
return self.getTrackDict(track)
else:
return {}
except Exception as ex:
raise click.ClickException(f"TrackController.getTrackDescriptor(): {repr(ex)}")
finally:
s.close()
def deleteTrack(self, trackId): def deleteTrack(self, trackId):
try: try:
s = self.Session() s = self.Session()
@@ -155,15 +151,15 @@ class TrackController():
if q.count(): if q.count():
trackDescriptor = self.getTrackDict(q.first()) #trackDescriptor = self.getTrackDict(q.first())
track = q.first()
q_siblings = s.query(Track).filter(Track.pattern_id == track.getPatternId(), Track.track_type == track.getType().index()).order_by(Track.sub_index)
q_siblings = s.query(Track).filter(Track.pattern_id == int(trackDescriptor['pattern_id']), Track.track_type == trackDescriptor['type'].value).order_by(Track.sub_index)
subIndex = 0 subIndex = 0
for track in q_siblings.all(): for track in q_siblings.all():
if track.sub_index == trackDescriptor['sub_index']: if track.sub_index == track.getSubIndex():
s.delete(track) s.delete(track)
else: else:
track.sub_index = subIndex track.sub_index = subIndex

View File

@@ -7,6 +7,7 @@ from textual.widgets import Header, Footer, Placeholder, Label, ListView, ListIt
from textual.containers import Grid, Horizontal from textual.containers import Grid, Horizontal
from ffx.model.pattern import Pattern from ffx.model.pattern import Pattern
from ffx.track_descriptor import TrackDescriptor
# from .show_controller import ShowController # from .show_controller import ShowController
# from .pattern_controller import PatternController # from .pattern_controller import PatternController
@@ -54,26 +55,26 @@ class TrackDeleteScreen(Screen):
} }
""" """
def __init__(self, trackId = None): def __init__(self, trackDescriptor : TrackDescriptor):
super().__init__() super().__init__()
self.context = self.app.getContext() self.context = self.app.getContext()
self.Session = self.context['database_session'] # convenience self.Session = self.context['database']['session'] # convenience
if trackId is None: if type(trackDescriptor) is not TrackDescriptor:
raise click.ClickException('TrackDeleteScreen.init(): trackId is required to be set') raise click.ClickException('TrackDeleteScreen.init(): trackDescriptor is required to be of type TrackDescriptor')
self.__tc = TrackController(context = self.context) self.__tc = TrackController(context = self.context)
self.track_obj = self.__tc.getTrackDescriptor(trackId) self.__trackDescriptor = trackDescriptor
def on_mount(self): def on_mount(self):
self.query_one("#subindexlabel", Static).update(str(self.track_obj['sub_index'])) self.query_one("#subindexlabel", Static).update(str(self.__trackDescriptor.getSubIndex()))
self.query_one("#patternlabel", Static).update(str(self.track_obj['pattern_id'])) self.query_one("#patternlabel", Static).update(str(self.__trackDescriptor.getPatternId()))
self.query_one("#languagelabel", Static).update(str(self.track_obj['language'].label())) self.query_one("#languagelabel", Static).update(str(self.__trackDescriptor.getLanguage().label()))
self.query_one("#titlelabel", Static).update(str(str(self.track_obj['title']))) self.query_one("#titlelabel", Static).update(str(str(self.__trackDescriptor.getTitle())))
def compose(self): def compose(self):
@@ -83,7 +84,7 @@ class TrackDeleteScreen(Screen):
with Grid(): with Grid():
#1 #1
yield Static(f"Are you sure to delete the following {self.track_obj['type'].label()} track?", id="toplabel", classes="four") yield Static(f"Are you sure to delete the following {self.__trackDescriptor.getType().label()} track?", id="toplabel", classes="four")
#2 #2
yield Static("sub index") yield Static("sub index")
@@ -122,12 +123,15 @@ class TrackDeleteScreen(Screen):
if event.button.id == "delete_button": if event.button.id == "delete_button":
trackId = self.__tc.findTrack(self.track_obj['pattern_id'], self.track_obj['type'], self.track_obj['sub_index']) track = self.__tc.findTrack(self.__trackDescriptor.getPatternId(), self.__trackDescriptor.getType(), self.__trackDescriptor.getSubIndex())
if trackId is not None: if track is None:
raise click.ClickException(f"Track is none: patternId={self.__trackDescriptor.getPatternId()} type={self.__trackDescriptor.getType()} subIndex={self.__trackDescriptor.getSubIndex()}")
if self.__tc.deleteTrack(trackId): if track is not None:
self.dismiss(self.track_obj)
if self.__tc.deleteTrack(track.getId()):
self.dismiss(self.__trackDescriptor)
else: else:
#TODO: Meldung #TODO: Meldung

View File

@@ -1,56 +1,170 @@
from language_data import LanguageData from .iso_language import IsoLanguage
from stream_type import StreamType from .track_type import TrackType
from .audio_layout import AudioLayout
from .track_disposition import TrackDisposition
class StreamDescriptor():
def __init__(self, class TrackDescriptor():
streamType : StreamType,
language : LanguageData,
title : str,
codec : str,
subIndex : int = -1):
self.__streamType = streamType INDEX_KEY = 'index'
self.__subIndex = subIndex SUB_INDEX_KEY = 'sub_index'
PATTERN_ID_KEY = 'pattern_id'
self.__streamLanguage = language TRACK_TYPE_KEY = 'track_type'
self.__streamTitle = title DISPOSITION_SET_KEY = 'disposition_set'
TAGS_KEY = 'tags'
AUDIO_LAYOUT_KEY = 'audio_layout'
self.__codecName = codec FFPROBE_DISPOSITION_KEY = 'disposition'
FFPROBE_TAGS_KEY = 'tags'
# "index": 4, def __init__(self, **kwargs):
# "codec_name": "hdmv_pgs_subtitle",
# "codec_long_name": "HDMV Presentation Graphic Stream subtitles", if TrackDescriptor.PATTERN_ID_KEY in kwargs.keys():
# "codec_type": "subtitle", if type(kwargs[TrackDescriptor.PATTERN_ID_KEY]) is not int:
# "codec_tag_string": "[0][0][0][0]", raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.PATTERN_ID_KEY} is required to be of type int")
# "codec_tag": "0x0000", self.__patternId = kwargs[TrackDescriptor.PATTERN_ID_KEY]
# "r_frame_rate": "0/0", else:
# "avg_frame_rate": "0/0", self.__patternId = -1
# "time_base": "1/1000",
# "start_pts": 0, if TrackDescriptor.INDEX_KEY in kwargs.keys():
# "start_time": "0.000000", if type(kwargs[TrackDescriptor.INDEX_KEY]) is not int:
# "duration_ts": 1421035, raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.INDEX_KEY} is required to be of type int")
# "duration": "1421.035000", self.__index = kwargs[TrackDescriptor.INDEX_KEY]
# "disposition": { else:
# "default": 1, self.__index = -1
# "dub": 0,
# "original": 0, if TrackDescriptor.SUB_INDEX_KEY in kwargs.keys():
# "comment": 0, if type(kwargs[TrackDescriptor.SUB_INDEX_KEY]) is not int:
# "lyrics": 0, raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.SUB_INDEX_KEY} is required to be of type dict")
# "karaoke": 0, self.__subIndex = kwargs[TrackDescriptor.SUB_INDEX_KEY]
# "forced": 0, else:
# "hearing_impaired": 0, self.__subIndex = -1
# "visual_impaired": 0,
# "clean_effects": 0,
# "attached_pic": 0, if TrackDescriptor.TRACK_TYPE_KEY in kwargs.keys():
# "timed_thumbnails": 0, if type(kwargs[TrackDescriptor.TRACK_TYPE_KEY]) is not TrackType:
# "non_diegetic": 0, raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.TRACK_TYPE_KEY} is required to be of type TrackType")
# "captions": 0, self.__trackType = kwargs[TrackDescriptor.TRACK_TYPE_KEY]
# "descriptions": 0, else:
# "metadata": 0, self.__trackType = TrackType.UNKNOWN
# "dependent": 0,
# "still_image": 0 if TrackDescriptor.TAGS_KEY in kwargs.keys():
# }, if type(kwargs[TrackDescriptor.TAGS_KEY]) is not dict:
# "tags": { raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.TAGS_KEY} is required to be of type dict")
# "language": "ger", self.__trackTags = kwargs[TrackDescriptor.TAGS_KEY]
# "title": "German Full" else:
self.__trackTags = {}
if TrackDescriptor.DISPOSITION_SET_KEY in kwargs.keys():
if type(kwargs[TrackDescriptor.DISPOSITION_SET_KEY]) is not set:
raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.DISPOSITION_SET_KEY} is required to be of type set")
for d in kwargs[TrackDescriptor.DISPOSITION_SET_KEY]:
if type(d) is not TrackDisposition:
raise TypeError(f"TrackDesciptor.__init__(): All elements of argument set {TrackDescriptor.DISPOSITION_SET_KEY} is required to be of type TrackDisposition")
self.__dispositionSet = kwargs[TrackDescriptor.DISPOSITION_SET_KEY]
else:
self.__dispositionSet = set()
if TrackDescriptor.AUDIO_LAYOUT_KEY in kwargs.keys():
if type(kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY]) is not AudioLayout:
raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.AUDIO_LAYOUT_KEY} is required to be of type AudioLayout")
self.__audioLayout = kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY]
else:
self.__audioLayout = AudioLayout.LAYOUT_UNDEFINED
@classmethod
def fromFfprobe(cls, streamObj):
"""Processes ffprobe stream data as array with elements according to the following example
{
"index": 4,
"codec_name": "hdmv_pgs_subtitle",
"codec_long_name": "HDMV Presentation Graphic Stream subtitles",
"codec_type": "subtitle",
"codec_tag_string": "[0][0][0][0]",
"codec_tag": "0x0000",
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/1000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 1421035,
"duration": "1421.035000",
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0,
"non_diegetic": 0,
"captions": 0,
"descriptions": 0,
"metadata": 0,
"dependent": 0,
"still_image": 0
},
"tags": {
"language": "ger",
"title": "German Full"
}
}
"""
trackType = TrackType.fromLabel(streamObj['codec_type']) if 'codec_type' in streamObj.keys() else TrackType.UNKNOWN
if trackType != TrackType.UNKNOWN:
kwargs = {}
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = trackType
kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = {t for d in (k for (k,v) in streamObj[TrackDescriptor.FFPROBE_DISPOSITION_KEY].items() if v)
if (t := TrackDisposition.find(d)) if t is not None} if TrackDescriptor.FFPROBE_DISPOSITION_KEY in streamObj.keys() else set()
kwargs[TrackDescriptor.TAGS_KEY] = streamObj[TrackDescriptor.FFPROBE_TAGS_KEY] if TrackDescriptor.FFPROBE_TAGS_KEY in streamObj.keys() else {}
kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = AudioLayout.identify(streamObj) if trackType == TrackType.AUDIO.label() else AudioLayout.LAYOUT_UNDEFINED
return cls(**kwargs)
else:
return None
def getPatternId(self):
return self.__patternId
def getIndex(self):
return self.__index
def getSubIndex(self):
return self.__subIndex
def getType(self):
return self.__trackType
def getLanguage(self):
if 'language' in self.__trackTags.keys():
return IsoLanguage.findThreeLetter(self.__trackTags['language'])
else:
return IsoLanguage.UNDEFINED
def getTitle(self):
if 'title' in self.__trackTags.keys():
return str(self.__trackTags['title'])
else:
return ''
def getAudioLayout(self):
return self.__audioLayout
def getTags(self):
return self.__trackTags
def getDispositionSet(self):
return self.__dispositionSet

View File

@@ -18,6 +18,9 @@ from .track_type import TrackType
from .iso_language import IsoLanguage from .iso_language import IsoLanguage
from .track_disposition import TrackDisposition from .track_disposition import TrackDisposition
from .audio_layout import AudioLayout
from .track_descriptor import TrackDescriptor
# Screen[dict[int, str, int]] # Screen[dict[int, str, int]]
@@ -75,59 +78,68 @@ class TrackDetailsScreen(Screen):
} }
""" """
def __init__(self, trackId = None, patternId = None, trackType : TrackType = None, subIndex = None): def __init__(self, trackDescriptor = None, patternId = None, trackType : TrackType = None, subIndex = None):
super().__init__() super().__init__()
self.context = self.app.getContext() self.context = self.app.getContext()
self.Session = self.context['database_session'] # convenience self.Session = self.context['database']['session'] # convenience
self.__tc = TrackController(context = self.context) self.__tc = TrackController(context = self.context)
self.__pc = PatternController(context = self.context) self.__pc = PatternController(context = self.context)
self.track_obj = self.__tc.getTrackDescriptor(trackId) if trackId is not None else {}
if self.track_obj: INDEX_KEY = 'index'
self.trackType = self.track_obj['type'] SUB_INDEX_KEY = 'sub_index'
self.subIndex = self.track_obj['sub_index'] PATTERN_ID_KEY = 'pattern_id'
self.pattern_obj = self.__pc.getPatternDescriptor(self.track_obj['pattern_id'])
self.track_obj['is_new'] = False TRACK_TYPE_KEY = 'track_type'
DISPOSITION_SET_KEY = 'disposition_set'
TAGS_KEY = 'tags'
AUDIO_LAYOUT_KEY = 'audio_layout'
# if trackDescriptor is None:
# self.__trackDescriptor = TrackDescriptor(index=,
# sub_index=
# pattern_id=patternId,
# track_type=trackType)
# else:
self.__isNew = trackDescriptor is None
if self.__isNew:
self.__trackType = trackType
self.__subIndex = subIndex
self.__trackDescriptor = None
self.__pattern = self.__pc.getPattern(patternId) if patternId is not None else {}
else: else:
self.trackType = trackType self.__trackType = trackDescriptor.getType()
self.subIndex = subIndex self.__subIndex = trackDescriptor.getSubIndex()
self.pattern_obj = self.__pc.getPatternDescriptor(patternId) if patternId is not None else {} self.__trackDescriptor = trackDescriptor
self.track_obj['is_new'] = True self.__pattern = self.__pc.getPattern(self.__trackDescriptor.getPatternId())
if self.trackType is None:
raise click.ClickException('Track type is required to be set')
if self.subIndex is None:
raise click.ClickException('Sub index for track is required to be set')
def on_mount(self): def on_mount(self):
if self.pattern_obj: if self.__pattern is not None:
self.query_one("#patternlabel", Static).update(str(self.pattern_obj['pattern'])) self.query_one("#patternlabel", Static).update(self.__pattern.getPattern())
if self.subIndex is not None: if self.__subIndex is not None:
self.query_one("#subindexlabel", Static).update(str(self.subIndex)) self.query_one("#subindexlabel", Static).update(str(self.__subIndex))
for d in TrackDisposition: for d in TrackDisposition:
dispositionIsSet = (self.track_obj dispositionIsSet = (self.__trackDescriptor is not None
and 'disposition_list' in self.track_obj.keys() and d in self.__trackDescriptor.getDispositionSet())
and d in self.track_obj['disposition_list'])
disposition = (d.label(), d.index(), dispositionIsSet) dispositionOption = (d.label(), d.index(), dispositionIsSet)
self.query_one("#dispositions_selection_list", SelectionList).add_option(disposition) self.query_one("#dispositions_selection_list", SelectionList).add_option(dispositionOption)
if 'language' in self.track_obj.keys(): if self.__trackDescriptor is not None:
self.query_one("#language_select", Select).value = self.track_obj['language'].label()
if 'title' in self.track_obj.keys():
self.query_one("#title_input", Input).value = str(self.track_obj['title'])
self.query_one("#language_select", Select).value = self.__trackDescriptor.getLanguage().label()
self.query_one("#title_input", Input).value = self.__trackDescriptor.getTitle()
def compose(self): def compose(self):
@@ -148,7 +160,7 @@ class TrackDetailsScreen(Screen):
with Grid(): with Grid():
# 1 # 1
yield Static(f"New {self.trackType.label()} stream" if self.track_obj['is_new'] else f"Edit {self.trackType.label()} stream", id="toplabel", classes="five") yield Static(f"New {self.__trackType.label()} stream" if self.__isNew else f"Edit {self.__trackType.label()} stream", id="toplabel", classes="five")
# 2 # 2
yield Static("for pattern") yield Static("for pattern")
@@ -220,50 +232,57 @@ class TrackDetailsScreen(Screen):
def getTrackDescriptorFromInput(self): def getTrackDescriptorFromInput(self):
trackDescriptor = {} kwargs = {}
trackDescriptor['pattern_id'] = int(self.pattern_obj['id']) kwargs[TrackDescriptor.PATTERN_ID_KEY] = int(self.__pattern.getId())
trackDescriptor['type'] = TrackType(self.trackType) kwargs[TrackDescriptor.INDEX_KEY] = -1
trackDescriptor['sub_index'] = self.subIndex kwargs[TrackDescriptor.SUB_INDEX_KEY] = self.__subIndex
trackDescriptor['language'] = IsoLanguage.find(str(self.query_one("#language_select", Select).value)) kwargs[TrackDescriptor.TRACK_TYPE_KEY] = self.__trackType
trackDescriptor['title'] = str(self.query_one("#title_input", Input).value)
trackTags = {}
language = self.query_one("#language_select", Select).value
# raise click.ClickException(f"language={language}")
if language:
trackTags['language'] = IsoLanguage.find(language).threeLetter()
title = self.query_one("#title_input", Input).value
if title:
trackTags['title'] = title
kwargs[TrackDescriptor.TAGS_KEY] = trackTags
disposition_flags = sum([2**f for f in self.query_one("#dispositions_selection_list", SelectionList).selected]) dispositionFlags = sum([2**f for f in self.query_one("#dispositions_selection_list", SelectionList).selected])
kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = TrackDisposition.toSet(dispositionFlags)
trackDescriptor['disposition_list'] = TrackDisposition.toList(disposition_flags) kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = AudioLayout.LAYOUT_UNDEFINED
return TrackDescriptor(**kwargs)
return trackDescriptor
# Event handler for button press # Event handler for button press
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:
# Check if the button pressed is the one we are interested in # Check if the button pressed is the one we are interested in
if event.button.id == "save_button": if event.button.id == "save_button":
trackDescriptor = self.getTrackDescriptorFromInput() trackDescriptor = self.getTrackDescriptorFromInput()
# Check for multiple default/forced disposition flags # Check for multiple default/forced disposition flags
trackIdList = self.__tc.findAllTracks(self.pattern_obj['id'])[self.trackType.label()] if self.__trackType == TrackType.AUDIO:
trackList = self.__tc.findAudioTracks(self.__pattern.getId())
elif self.__trackType == TrackType.SUBTITLE:
trackList = self.__tc.findSubtitleTracks(self.__pattern.getId())
else:
trackList = []
descriptorList = [d for d in (self.__tc.getTrackDescriptor(t) for t in trackIdList) siblingTrackList = [t for t in trackList if t.getType() == self.__trackType and t.getSubIndex() != self.__subIndex]
if d['type'] == self.trackType
and d['sub_index'] != self.subIndex]
numDefaultTracks = len([t for t in siblingTrackList if TrackDisposition.DEFAULT in t.getDispositionSet()])
numForcedTracks = len([t for t in siblingTrackList if TrackDisposition.FORCED in t.getDispositionSet()])
numDefaultTracks = [d for d in descriptorList if TrackDisposition.DEFAULT in d['disposition_list']] if ((TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() and numDefaultTracks)
numForcedTracks = [d for d in descriptorList if TrackDisposition.FORCED in d['disposition_list']] or (TrackDisposition.FORCED in trackDescriptor.getDispositionSet() and numForcedTracks)):
doubleDefaultOrForced = ((TrackDisposition.DEFAULT in trackDescriptor['disposition_list'] and numDefaultTracks)
or (TrackDisposition.FORCED in trackDescriptor['disposition_list'] and numForcedTracks))
if doubleDefaultOrForced:
self.query_one("#messagestatic", Static).update("Cannot add another stream with disposition flag 'debug' or 'forced' set") self.query_one("#messagestatic", Static).update("Cannot add another stream with disposition flag 'debug' or 'forced' set")
@@ -271,14 +290,14 @@ class TrackDetailsScreen(Screen):
self.query_one("#messagestatic", Static).update(" ") self.query_one("#messagestatic", Static).update(" ")
if self.track_obj['is_new']: if self.__isNew:
self.__tc.addTrack(trackDescriptor) self.__tc.addTrack(trackDescriptor)
self.dismiss(trackDescriptor) self.dismiss(trackDescriptor)
else: else:
trackId = self.__tc.findTrack(self.pattern_obj['id'], self.trackType, self.subIndex) trackId = self.__tc.findTrack(self.__pattern.getId(), self.__trackType, self.__subIndex)
if self.__tc.updateTrack(trackId, trackDescriptor): if self.__tc.updateTrack(trackId, trackDescriptor):
self.dismiss(trackDescriptor) self.dismiss(trackDescriptor)

View File

@@ -1,5 +1,7 @@
import difflib, click
from enum import Enum from enum import Enum
import difflib
class TrackDisposition(Enum): class TrackDisposition(Enum):
@@ -23,6 +25,7 @@ class TrackDisposition(Enum):
DEPENDENT = {"name": "dependent", "index": 16} DEPENDENT = {"name": "dependent", "index": 16}
STILL_IMAGE = {"name": "still_image", "index": 17} STILL_IMAGE = {"name": "still_image", "index": 17}
def label(self): def label(self):
return str(self.value['name']) return str(self.value['name'])
@@ -31,19 +34,32 @@ class TrackDisposition(Enum):
@staticmethod @staticmethod
def toFlags(dispositionList): def toFlags(dispositionSet):
"""Flags stored in integer bits (2**index)""" """Flags stored in integer bits (2**index)"""
if type(dispositionSet) is not set:
raise click.ClickException('TrackDisposition.toFlags(): Argument is not of type set')
flags = 0 flags = 0
for d in dispositionList: for d in dispositionSet:
if type(d) is not TrackDisposition:
raise click.ClickException('TrackDisposition.toFlags(): Element not of type TrackDisposition')
flags += 2 ** d.index() flags += 2 ** d.index()
return flags return flags
@staticmethod @staticmethod
def toList(flags): def toSet(flags):
dispositionSet = set()
dispositionList = []
for d in TrackDisposition: for d in TrackDisposition:
if flags & int(2 ** d.index()): if flags & int(2 ** d.index()):
dispositionList += [d] dispositionSet.add(d)
return dispositionList return dispositionSet
@staticmethod
def find(label):
matchingDispositions = [d for d in TrackDisposition if d.label() == str(label)]
if matchingDispositions:
return matchingDispositions[0]
else:
return None

View File

@@ -2,18 +2,33 @@ from enum import Enum
class TrackType(Enum): class TrackType(Enum):
VIDEO = 1 VIDEO = {'label': 'video', 'index': 1}
AUDIO = 2 AUDIO = {'label': 'audio', 'index': 2}
SUBTITLE = 3 SUBTITLE = {'label': 'subtitle', 'index': 3}
UNKNOWN = {'label': 'unknown', 'index': 0}
def label(self): def label(self):
"""Returns the stream type as string""" """Returns the stream type as string"""
return str(self.value['label'])
labels = { def index(self):
TrackType.VIDEO: "video", """Returns the stream type index"""
TrackType.AUDIO: "audio", return int(self.value['index'])
TrackType.SUBTITLE: "subtitle"
}
return labels.get(self, "undefined") @staticmethod
def fromLabel(label):
tlist = [t for t in TrackType if t.value['label'] == label]
if tlist:
return tlist[0]
else:
return TrackType.UNKNOWN
@staticmethod
def fromIndex(index):
tlist = [t for t in TrackType if t.value['index'] == index]
if tlist:
return tlist[0]
else:
return TrackType.UNKNOWN