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.database import databaseContext
from ffx.media_descriptor import MediaDescriptor
from ffx.file_properties import FileProperties
VERSION='0.1.0'
DEFAULT_VIDEO_ENCODER = 'vp9'
@@ -41,11 +47,6 @@ STREAM_TYPE_VIDEO = 'video'
STREAM_TYPE_AUDIO = 'audio'
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]+)'
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
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'
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):
"""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):
"""Generates ffmpeg options for one output audio stream including channel remapping, codec and bitrate"""
if layout == STREAM_LAYOUT_6_1:
return [f"-c:a:{index}",
'libopus',
f"-filter:a:{index}",
'channelmap=channel_layout=6.1',
f"-b:a:{index}",
context['bitrates']['dts']]
elif layout == STREAM_LAYOUT_5_1:
return [f"-c:a:{index}",
'libopus',
f"-filter:a:{index}",
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
f"-b:a:{index}",
context['bitrates']['ac3']]
elif layout == STREAM_LAYOUT_STEREO:
return [f"-c:a:{index}",
'libopus',
f"-b:a:{index}",
context['bitrates']['stereo']]
elif layout == STREAM_LAYOUT_6CH:
return [f"-c:a:{index}",
'libopus',
f"-filter:a:{index}",
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
f"-b:a:{index}",
context['bitrates']['ac3']]
else:
return []
pass
#
# if layout == STREAM_LAYOUT_6_1:
# return [f"-c:a:{index}",
# 'libopus',
# f"-filter:a:{index}",
# 'channelmap=channel_layout=6.1',
# f"-b:a:{index}",
# context['bitrates']['dts']]
#
# elif layout == STREAM_LAYOUT_5_1:
# return [f"-c:a:{index}",
# 'libopus',
# f"-filter:a:{index}",
# "channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
# f"-b:a:{index}",
# context['bitrates']['ac3']]
#
# elif layout == STREAM_LAYOUT_STEREO:
# return [f"-c:a:{index}",
# 'libopus',
# f"-b:a:{index}",
# context['bitrates']['stereo']]
#
# elif layout == STREAM_LAYOUT_6CH:
# return [f"-c:a:{index}",
# 'libopus',
# f"-filter:a:{index}",
# "channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
# f"-b:a:{index}",
# context['bitrates']['ac3']]
# else:
# return []
def generateClearTokens(streams):
@@ -334,8 +223,9 @@ def searchSubtitleFiles(dir, prefix):
@click.pass_context
def ffx(ctx):
"""FFX"""
ctx.obj = {}
pass
ctx.obj['database'] = databaseContext()
# 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.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()
@click.pass_context
@click.argument('filename', nargs=1)
def inspect(ctx, filename):
@click.argument('paths', nargs=-1)
@click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix')
# if 'database' not in ctx.obj.keys():
# ctx.obj['database'] = databaseContext()
try:
@click.option('-sd', '--subtitle-directory', type=str, default='', help='Load subtitles from here')
@click.option('-sp', '--subtitle-prefix', type=str, default='', help='Subtitle filename prefix')
fp = FileProperties(ctx.obj, filename)
md = fp.getMediaDescriptor()
@click.option("-o", "--output-directory", type=str, default='')
print(md.getTags())
@click.option("--dry-run", is_flag=True, default=False)
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)]
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")
# @ffx.command()
# @click.pass_context
#
# @click.argument('paths', nargs=-1)
# @click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix')
#
# @click.option('-sd', '--subtitle-directory', type=str, default='', help='Load subtitles from here')
# @click.option('-sp', '--subtitle-prefix', type=str, default='', help='Subtitle filename prefix')
#
# @click.option("-o", "--output-directory", type=str, default='')
#
# @click.option("--dry-run", is_flag=True, default=False)
#
#
# def unmux(ctx,
# label,
# paths,
# subtitle_directory,
# subtitle_prefix,
# 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):
# if 'database' not in ctx.obj.keys():
# ctx.obj['database'] = databaseContext()
app = FfxApp(ctx.obj)
app.run()
@@ -502,6 +413,10 @@ def convert(ctx,
context = ctx.obj
if 'database' not in context.keys():
context['database'] = databaseContext()
click.echo(f"\nVideo encoder: {video_encoder}")
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.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.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
from .shows_screen import ShowsScreen
from .warning_screen import WarningScreen
@@ -32,29 +35,6 @@ class FfxApp(App):
# Data 'input' variable
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:
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():
@@ -7,15 +18,18 @@ class FileProperties():
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[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
self.__sourceDirectory = os.path.dirname(sourcePath)
self.__sourceFilename = os.path.basename(sourcePath)
self.__sourcePath = sourcePath
self.__sourceDirectory = os.path.dirname(self.__sourcePath)
self.__sourceFilename = os.path.basename(self.__sourcePath)
sourcePathTokens = self.__sourceFilename.split('.')
if sourcePathTokens[-1] in FilenameController.FILE_EXTENSIONS:
if sourcePathTokens[-1] in FileProperties.FILE_EXTENSIONS:
self.__sourceFileBasename = '.'.join(sourcePathTokens[:-1])
self.__sourceFilenameExtension = sourcePathTokens[-1]
else:
@@ -23,8 +37,8 @@ class FileProperties():
self.__sourceFilenameExtension = ''
se_match = re.compile(FilenameController.SEASON_EPISODE_INDICATOR_MATCH)
e_match = re.compile(FilenameController.EPISODE_INDICATOR_MATCH)
se_match = re.compile(FileProperties.SEASON_EPISODE_INDICATOR_MATCH)
e_match = re.compile(FileProperties.EPISODE_INDICATOR_MATCH)
se_result = se_match.search(self.__sourceFilename)
e_result = e_match.search(self.__sourceFilename)
@@ -42,25 +56,165 @@ class FileProperties():
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 []
print(f"season={season} episode={episode} file={file_index}")
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}")
#
#
# # 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
targetFilenameTokens = []
targetFilenameExtension = DEFAULT_FILE_EXTENSION
def getFormatData(self):
"""
"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:
targetFilenameTokens = [label]
ffprobeOutput, ffprobeError, returnCode = executeProcess(["ffprobe",
"-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:
targetFilenameTokens = [sourceFileBasename]
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)['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"}
WELSH = {"name": "Welsh", "iso639_1": "cy", "iso639_2": "wel"}
UNDEFINED = {"name": "undefined", "iso639_1": "xx", "iso639_2": "und"}
@staticmethod
def find(label : str):
@@ -81,14 +83,14 @@ class IsoLanguage(Enum):
if closestMatches:
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:
return None
return IsoLanguage.UNDEFINED
@staticmethod
def findThreeLetter(theeLetter : str):
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):

View File

@@ -1,2 +1,43 @@
from ffx.track_type import TrackType
from ffx.track_descriptor import TrackDescriptor
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 .track import Track
from ffx.media_descriptor import MediaDescriptor
class Pattern(Base):
@@ -26,3 +27,50 @@ class Pattern(Base):
# show: Mapped["Show"] = relationship(back_populates="patterns")
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)
indicator_season_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.model.tag import Tag
from ffx.track_disposition import TrackDisposition
from ffx.track_descriptor import TrackDescriptor
import click
class Track(Base):
"""
@@ -29,6 +30,8 @@ class Track(Base):
# P=pattern_id+sub_index+track_type
track_type = Column(Integer) # TrackType
index = Column(Integer)
sub_index = Column(Integer)
# v1.x
@@ -36,14 +39,135 @@ class Track(Base):
pattern = relationship('Pattern', back_populates='tracks')
language = Column(String) # IsoLanguage threeLetter
title = Column(String)
# language = Column(String) # IsoLanguage threeLetter
# 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)
def getDescriptor(self):
pass
def __init__(self, **kwargs):
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 ffx.track_type import TrackType
class Tag(Base):
class TrackTag(Base):
"""
relationship(argument, opt1, opt2, ...)
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
"""
__tablename__ = 'tags'
__tablename__ = 'track_tags'
# v1.x
id = Column(Integer, primary_key=True)
@@ -27,4 +25,4 @@ class Tag(Base):
# v1.x
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 .media_descriptor import MediaDescriptor
class PatternController():
def __init__(self, context):
self.context = context
self.Session = self.context['database_session'] # convenience
self.Session = self.context['database']['session'] # convenience
def addPattern(self, patternDescriptor):
@@ -77,26 +79,24 @@ class PatternController():
s.close()
def getPatternDict(self, pattern):
patternDescriptor = {}
patternDescriptor['id'] = int(pattern.id)
patternDescriptor['pattern'] = str(pattern.pattern)
patternDescriptor['show_id'] = int(pattern.show_id)
return patternDescriptor
def getPattern(self, patternId : int):
def getPatternDescriptor(self, patternId):
if type(patternId) is not int:
raise ValueError(f"PatternController.getPattern(): Argument patternId is required to be of type int")
try:
s = self.Session()
q = s.query(Pattern).filter(Pattern.id == int(patternId))
if q.count():
pattern = q.first()
return self.getPatternDict(pattern)
# pattern = q.first()
#return self.getPatternDict(pattern)
return q.first()
else:
return None
except Exception as ex:
raise click.ClickException(f"PatternController.getPatternDescriptor(): {repr(ex)}")
raise click.ClickException(f"PatternController.getPattern(): {repr(ex)}")
finally:
s.close()
@@ -125,39 +125,62 @@ class PatternController():
def matchFilename(self, filename):
SEASON_PATTERN = '[sS]([0-9]+)'
EPISODE_PATTERN = '[eE]([0-9]+)'
#SEASON_PATTERN = '[sS]([0-9]+)'
#EPISODE_PATTERN = '[eE]([0-9]+)'
result = {}
#result = {}
try:
s = self.Session()
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 match:
if matchedPatterns:
return matchedPatterns[0]
else:
return None
result['pattern_id'] = pattern.id
result['show_id'] = pattern.show_id
result['indicator'] = match.group(1)
seasonMatch = re.search(SEASON_PATTERN, result['indicator'])
if seasonMatch:
result['season'] = int(seasonMatch.group(1))
episodeMatch = re.search(EPISODE_PATTERN, result['indicator'])
if episodeMatch:
result['episode'] = int(episodeMatch.group(1))
# for pattern in q.all():
#
# match = re.search(pattern.pattern, filename)
#
# if match:
#
# result['pattern_id'] = pattern.id
# result['show_id'] = pattern.show_id
#
# result['indicator'] = match.group(1)
#
# seasonMatch = re.search(SEASON_PATTERN, result['indicator'])
# if seasonMatch:
# 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:
raise click.ClickException(f"PatternController.matchFilename(): {repr(ex)}")
raise click.ClickException(f"PatternController.findPattern(): {repr(ex)}")
finally:
s.close()
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__()
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.__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_disposition import TrackDisposition
from ffx.track_descriptor import TrackDescriptor
from textual.widgets._data_table import CellDoesNotExist
@@ -73,13 +74,13 @@ class PatternDetailsScreen(Screen):
super().__init__()
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.__sc = ShowController(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 {}
@@ -98,7 +99,7 @@ class PatternDetailsScreen(Screen):
return [{'id': int(p.id), 'pattern': p.pattern} for p in q.all()]
except Exception as ex:
click.ClickException(f"loadPatterns(): {repr(ex)}")
raise click.ClickException(f"loadTracks(): {repr(ex)}")
finally:
s.close()
@@ -107,53 +108,53 @@ class PatternDetailsScreen(Screen):
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)
dispoList = ad['disposition_list']
for at in audioTracks:
row = (ad['sub_index'],
" ",
ad['language'].label(),
ad['title'],
'Yes' if TrackDisposition.DEFAULT in dispoList else 'No',
'Yes' if TrackDisposition.FORCED in dispoList else 'No')
dispoSet = at.getDispositionSet()
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):
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)
dispoList = sd['disposition_list']
for st in subtitleTracks:
row = (sd['sub_index'],
" ",
sd['language'].label(),
sd['title'],
'Yes' if TrackDisposition.DEFAULT in dispoList else 'No',
'Yes' if TrackDisposition.FORCED in dispoList else 'No')
dispoSet = st.getDispositionSet()
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):
if self.pattern_obj:
self.query_one("#pattern_input", Input).value = str(self.pattern_obj['pattern'])
if self.show_obj:
self.query_one("#showlabel", Static).update(f"{self.show_obj['id']} - {self.show_obj['name']} ({self.show_obj['year']})")
if self.pattern_obj:
if self.__pattern is not None:
self.query_one("#pattern_input", Input).value = str(self.__pattern.getPattern())
self.updateAudioTracks()
self.updateSubtitleTracks()
@@ -192,7 +193,7 @@ class PatternDetailsScreen(Screen):
with Grid():
# 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")
# 2
@@ -209,7 +210,7 @@ class PatternDetailsScreen(Screen):
yield Static("Audio streams")
yield Static(" ")
if self.pattern_obj:
if self.__pattern is not None:
yield Button("Add", id="button_add_audio_stream")
yield Button("Edit", id="button_edit_audio_stream")
yield Button("Delete", id="button_delete_audio_stream")
@@ -227,7 +228,7 @@ class PatternDetailsScreen(Screen):
yield Static("Subtitle streams")
yield Static(" ")
if self.pattern_obj:
if self.__pattern is not None:
yield Button("Add", id="button_add_subtitle_stream")
yield Button("Edit", id="button_edit_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
try:
@@ -269,7 +270,18 @@ class PatternDetailsScreen(Screen):
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:
return None
@@ -278,9 +290,9 @@ class PatternDetailsScreen(Screen):
return None
def getSelectedSubtitleTrackId(self):
def getSelectedSubtitleTrackDescriptor(self):
if not self.pattern_obj:
if not self.__pattern is None:
return None
try:
@@ -294,7 +306,18 @@ class PatternDetailsScreen(Screen):
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:
return None
@@ -313,9 +336,9 @@ class PatternDetailsScreen(Screen):
patternDescriptor['show_id'] = self.show_obj['id']
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)
else:
#TODO: Meldung
@@ -335,29 +358,28 @@ class PatternDetailsScreen(Screen):
# Save pattern when just created before adding streams
if self.pattern_obj:
#self.pattern_obj
if self.__pattern is not None:
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()
if selectedAudioTrackId is not None:
selectedAudioTrack = self.getSelectedAudioTrackDescriptor()
if selectedAudioTrack is not None:
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":
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":
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()
if selectedSubtitleTrackId is not None:
selectedSubtitleTrack = self.getSelectedSubtitleTrackDescriptor()
if selectedSubtitleTrack is not None:
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":
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":
@@ -373,63 +395,67 @@ class PatternDetailsScreen(Screen):
def handle_add_track(self, trackDescriptor):
dispoList = trackDescriptor['disposition_list']
if trackDescriptor['type'] == TrackType.AUDIO:
dispoSet = trackDescriptor.getDispositionSet()
trackType = trackDescriptor.getType()
subIndex = trackDescriptor.getSubIndex()
language = trackDescriptor.getLanguage()
title = trackDescriptor.getTitle()
row = (trackDescriptor['sub_index'],
if trackType == TrackType.AUDIO:
row = (subIndex,
" ",
trackDescriptor['language'].label(),
trackDescriptor['title'],
'Yes' if TrackDisposition.DEFAULT in dispoList else 'No',
'Yes' if TrackDisposition.FORCED in dispoList else 'No')
language.label(),
title,
'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
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(),
trackDescriptor['title'],
'Yes' if TrackDisposition.DEFAULT in dispoList else 'No',
'Yes' if TrackDisposition.FORCED in dispoList else 'No')
language.label(),
title,
'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
self.subtitleStreamsTable.add_row(*map(str, row))
def handle_edit_track(self, trackDescriptor):
def handle_edit_track(self, trackDescriptor : TrackDescriptor):
try:
if trackDescriptor['type'] == TrackType.AUDIO:
try:
if trackDescriptor.getType() == TrackType.AUDIO:
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_title, trackDescriptor['title'])
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_forced, 'Yes' if TrackDisposition.FORCED in trackDescriptor['disposition_list'] else 'No')
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.getTitle())
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.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)
self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_language, trackDescriptor['language'].label())
self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_title, trackDescriptor['title'])
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_forced, 'Yes' if TrackDisposition.FORCED in trackDescriptor['disposition_list'] else 'No')
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.getTitle())
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.getDispositionSet() else 'No')
except CellDoesNotExist:
pass
def handle_delete_track(self, trackDescriptor):
def handle_delete_track(self, trackDescriptor : TrackDescriptor):
try:
if trackDescriptor['type'] == TrackType.AUDIO:
if trackDescriptor.getType() == TrackType.AUDIO:
self.updateAudioTracks()
if trackDescriptor['type'] == TrackType.SUBTITLE:
if trackDescriptor.getType() == TrackType.SUBTITLE:
self.updateSubtitleTracks()
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):
self.context = context
self.Session = self.context['database_session'] # convenience
self.Session = self.context['database']['session'] # convenience
def getShowDesciptor(self, showId):
@@ -22,16 +22,19 @@ class ShowController():
if q.count():
show = q.first()
showDescriptor['id'] = int(show.id)
showDescriptor['name'] = str(show.name)
showDescriptor['year'] = int(show.year)
# showDescriptor['id'] = int(show.id)
# showDescriptor['name'] = str(show.name)
# 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
return show.getDesciptor()
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
except Exception as ex:
raise click.ClickException(f"ShowController.getShowDesciptor(): {repr(ex)}")

View File

@@ -48,7 +48,7 @@ class ShowDeleteScreen(Screen):
super().__init__()
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.show_obj = self.__sc.getShowDesciptor(showId) if showId is not None else {}

View File

@@ -76,7 +76,7 @@ class ShowDetailsScreen(Screen):
super().__init__()
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.__pc = PatternController(context = self.context)

View File

@@ -57,7 +57,7 @@ class ShowsScreen(Screen):
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 ffx.model.track_tag import TrackTag
class TrackController():
def __init__(self, context):
self.context = context
self.Session = self.context['database_session'] # convenience
self.Session = self.context['database']['session'] # convenience
def addTrack(self, trackDescriptor):
@@ -23,21 +25,22 @@ class TrackController():
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'])))
track = Track(pattern_id = int(trackDescriptor.getPatternId()),
track_type = int(trackDescriptor.getType().index()),
sub_index = int(trackDescriptor.getSubIndex()),
disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet())))
s.add(track)
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:
raise click.ClickException(f"TrackController.addTrack(): {repr(ex)}")
finally:
@@ -55,9 +58,9 @@ class TrackController():
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']))
# track.language = str(trackDescriptor['language'].threeLetter())
# track.title = str(trackDescriptor['title'])
track.disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet()))
s.commit()
@@ -71,44 +74,68 @@ class TrackController():
finally:
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:
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.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
q = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == TrackType.AUDIO.index())
return [a for a in q.all()]
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:
s.close()
def findTrack(self, patternId, trackType : TrackType, subIndex):
def findTrack(self, patternId : int, trackType : TrackType, subIndex : int):
try:
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():
track = q.first()
return int(track.id)
#track = q.first()
#return int(track.id)
return q.first()
else:
return None
@@ -117,37 +144,6 @@ class TrackController():
finally:
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):
try:
s = self.Session()
@@ -155,15 +151,15 @@ class TrackController():
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 == int(trackDescriptor['pattern_id']), Track.track_type == trackDescriptor['type'].value).order_by(Track.sub_index)
q_siblings = s.query(Track).filter(Track.pattern_id == track.getPatternId(), Track.track_type == track.getType().index()).order_by(Track.sub_index)
subIndex = 0
for track in q_siblings.all():
if track.sub_index == trackDescriptor['sub_index']:
if track.sub_index == track.getSubIndex():
s.delete(track)
else:
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 ffx.model.pattern import Pattern
from ffx.track_descriptor import TrackDescriptor
# from .show_controller import ShowController
# from .pattern_controller import PatternController
@@ -54,26 +55,26 @@ class TrackDeleteScreen(Screen):
}
"""
def __init__(self, trackId = None):
def __init__(self, trackDescriptor : TrackDescriptor):
super().__init__()
self.context = self.app.getContext()
self.Session = self.context['database_session'] # convenience
self.Session = self.context['database']['session'] # convenience
if trackId is None:
raise click.ClickException('TrackDeleteScreen.init(): trackId is required to be set')
if type(trackDescriptor) is not TrackDescriptor:
raise click.ClickException('TrackDeleteScreen.init(): trackDescriptor is required to be of type TrackDescriptor')
self.__tc = TrackController(context = self.context)
self.track_obj = self.__tc.getTrackDescriptor(trackId)
self.__trackDescriptor = trackDescriptor
def on_mount(self):
self.query_one("#subindexlabel", Static).update(str(self.track_obj['sub_index']))
self.query_one("#patternlabel", Static).update(str(self.track_obj['pattern_id']))
self.query_one("#languagelabel", Static).update(str(self.track_obj['language'].label()))
self.query_one("#titlelabel", Static).update(str(str(self.track_obj['title'])))
self.query_one("#subindexlabel", Static).update(str(self.__trackDescriptor.getSubIndex()))
self.query_one("#patternlabel", Static).update(str(self.__trackDescriptor.getPatternId()))
self.query_one("#languagelabel", Static).update(str(self.__trackDescriptor.getLanguage().label()))
self.query_one("#titlelabel", Static).update(str(str(self.__trackDescriptor.getTitle())))
def compose(self):
@@ -83,7 +84,7 @@ class TrackDeleteScreen(Screen):
with Grid():
#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
yield Static("sub index")
@@ -122,12 +123,15 @@ class TrackDeleteScreen(Screen):
if event.button.id == "delete_button":
trackId = self.__tc.findTrack(self.track_obj['pattern_id'], self.track_obj['type'], self.track_obj['sub_index'])
if trackId is not None:
track = self.__tc.findTrack(self.__trackDescriptor.getPatternId(), self.__trackDescriptor.getType(), self.__trackDescriptor.getSubIndex())
if self.__tc.deleteTrack(trackId):
self.dismiss(self.track_obj)
if track is None:
raise click.ClickException(f"Track is none: patternId={self.__trackDescriptor.getPatternId()} type={self.__trackDescriptor.getType()} subIndex={self.__trackDescriptor.getSubIndex()}")
if track is not None:
if self.__tc.deleteTrack(track.getId()):
self.dismiss(self.__trackDescriptor)
else:
#TODO: Meldung

View File

@@ -1,56 +1,170 @@
from language_data import LanguageData
from stream_type import StreamType
from .iso_language import IsoLanguage
from .track_type import TrackType
from .audio_layout import AudioLayout
from .track_disposition import TrackDisposition
class StreamDescriptor():
def __init__(self,
streamType : StreamType,
language : LanguageData,
title : str,
codec : str,
subIndex : int = -1):
class TrackDescriptor():
INDEX_KEY = 'index'
SUB_INDEX_KEY = 'sub_index'
PATTERN_ID_KEY = 'pattern_id'
self.__streamType = streamType
self.__subIndex = subIndex
TRACK_TYPE_KEY = 'track_type'
DISPOSITION_SET_KEY = 'disposition_set'
TAGS_KEY = 'tags'
AUDIO_LAYOUT_KEY = 'audio_layout'
self.__streamLanguage = language
self.__streamTitle = title
FFPROBE_DISPOSITION_KEY = 'disposition'
FFPROBE_TAGS_KEY = 'tags'
self.__codecName = codec
def __init__(self, **kwargs):
# "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"
if TrackDescriptor.PATTERN_ID_KEY in kwargs.keys():
if type(kwargs[TrackDescriptor.PATTERN_ID_KEY]) is not int:
raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.PATTERN_ID_KEY} is required to be of type int")
self.__patternId = kwargs[TrackDescriptor.PATTERN_ID_KEY]
else:
self.__patternId = -1
if TrackDescriptor.INDEX_KEY in kwargs.keys():
if type(kwargs[TrackDescriptor.INDEX_KEY]) is not int:
raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.INDEX_KEY} is required to be of type int")
self.__index = kwargs[TrackDescriptor.INDEX_KEY]
else:
self.__index = -1
if TrackDescriptor.SUB_INDEX_KEY in kwargs.keys():
if type(kwargs[TrackDescriptor.SUB_INDEX_KEY]) is not int:
raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.SUB_INDEX_KEY} is required to be of type dict")
self.__subIndex = kwargs[TrackDescriptor.SUB_INDEX_KEY]
else:
self.__subIndex = -1
if TrackDescriptor.TRACK_TYPE_KEY in kwargs.keys():
if type(kwargs[TrackDescriptor.TRACK_TYPE_KEY]) is not TrackType:
raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.TRACK_TYPE_KEY} is required to be of type TrackType")
self.__trackType = kwargs[TrackDescriptor.TRACK_TYPE_KEY]
else:
self.__trackType = TrackType.UNKNOWN
if TrackDescriptor.TAGS_KEY in kwargs.keys():
if type(kwargs[TrackDescriptor.TAGS_KEY]) is not dict:
raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.TAGS_KEY} is required to be of type dict")
self.__trackTags = kwargs[TrackDescriptor.TAGS_KEY]
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 .track_disposition import TrackDisposition
from .audio_layout import AudioLayout
from .track_descriptor import TrackDescriptor
# 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__()
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.__pc = PatternController(context = self.context)
self.track_obj = self.__tc.getTrackDescriptor(trackId) if trackId is not None else {}
INDEX_KEY = 'index'
SUB_INDEX_KEY = 'sub_index'
PATTERN_ID_KEY = 'pattern_id'
if self.track_obj:
self.trackType = self.track_obj['type']
self.subIndex = self.track_obj['sub_index']
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:
self.trackType = trackType
self.subIndex = subIndex
self.pattern_obj = self.__pc.getPatternDescriptor(patternId) if patternId is not None else {}
self.track_obj['is_new'] = True
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')
self.__trackType = trackDescriptor.getType()
self.__subIndex = trackDescriptor.getSubIndex()
self.__trackDescriptor = trackDescriptor
self.__pattern = self.__pc.getPattern(self.__trackDescriptor.getPatternId())
def on_mount(self):
if self.pattern_obj:
self.query_one("#patternlabel", Static).update(str(self.pattern_obj['pattern']))
if self.__pattern is not None:
self.query_one("#patternlabel", Static).update(self.__pattern.getPattern())
if self.subIndex is not None:
self.query_one("#subindexlabel", Static).update(str(self.subIndex))
if self.__subIndex is not None:
self.query_one("#subindexlabel", Static).update(str(self.__subIndex))
for d in TrackDisposition:
dispositionIsSet = (self.track_obj
and 'disposition_list' in self.track_obj.keys()
and d in self.track_obj['disposition_list'])
dispositionIsSet = (self.__trackDescriptor is not None
and d in self.__trackDescriptor.getDispositionSet())
disposition = (d.label(), d.index(), dispositionIsSet)
self.query_one("#dispositions_selection_list", SelectionList).add_option(disposition)
dispositionOption = (d.label(), d.index(), dispositionIsSet)
self.query_one("#dispositions_selection_list", SelectionList).add_option(dispositionOption)
if 'language' in self.track_obj.keys():
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'])
if self.__trackDescriptor is not None:
self.query_one("#language_select", Select).value = self.__trackDescriptor.getLanguage().label()
self.query_one("#title_input", Input).value = self.__trackDescriptor.getTitle()
def compose(self):
@@ -148,7 +160,7 @@ class TrackDetailsScreen(Screen):
with Grid():
# 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
yield Static("for pattern")
@@ -220,50 +232,57 @@ class TrackDetailsScreen(Screen):
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)
trackDescriptor['sub_index'] = self.subIndex
kwargs[TrackDescriptor.INDEX_KEY] = -1
kwargs[TrackDescriptor.SUB_INDEX_KEY] = self.__subIndex
trackDescriptor['language'] = IsoLanguage.find(str(self.query_one("#language_select", Select).value))
trackDescriptor['title'] = str(self.query_one("#title_input", Input).value)
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = self.__trackType
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
return TrackDescriptor(**kwargs)
# Event handler for button press
def on_button_pressed(self, event: Button.Pressed) -> None:
# Check if the button pressed is the one we are interested in
if event.button.id == "save_button":
trackDescriptor = self.getTrackDescriptorFromInput()
# 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)
if d['type'] == self.trackType
and d['sub_index'] != self.subIndex]
siblingTrackList = [t for t in trackList if t.getType() == self.__trackType and t.getSubIndex() != 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']]
numForcedTracks = [d for d in descriptorList if TrackDisposition.FORCED in d['disposition_list']]
doubleDefaultOrForced = ((TrackDisposition.DEFAULT in trackDescriptor['disposition_list'] and numDefaultTracks)
or (TrackDisposition.FORCED in trackDescriptor['disposition_list'] and numForcedTracks))
if doubleDefaultOrForced:
if ((TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() and numDefaultTracks)
or (TrackDisposition.FORCED in trackDescriptor.getDispositionSet() and numForcedTracks)):
self.query_one("#messagestatic", Static).update("Cannot add another stream with disposition flag 'debug' or 'forced' set")
@@ -271,15 +290,15 @@ class TrackDetailsScreen(Screen):
self.query_one("#messagestatic", Static).update(" ")
if self.track_obj['is_new']:
if self.__isNew:
self.__tc.addTrack(trackDescriptor)
self.dismiss(trackDescriptor)
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):
self.dismiss(trackDescriptor)

View File

@@ -1,5 +1,7 @@
import difflib, click
from enum import Enum
import difflib
class TrackDisposition(Enum):
@@ -23,6 +25,7 @@ class TrackDisposition(Enum):
DEPENDENT = {"name": "dependent", "index": 16}
STILL_IMAGE = {"name": "still_image", "index": 17}
def label(self):
return str(self.value['name'])
@@ -31,19 +34,32 @@ class TrackDisposition(Enum):
@staticmethod
def toFlags(dispositionList):
def toFlags(dispositionSet):
"""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
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()
return flags
@staticmethod
def toList(flags):
dispositionList = []
def toSet(flags):
dispositionSet = set()
for d in TrackDisposition:
if flags & int(2 ** d.index()):
dispositionList += [d]
return dispositionList
dispositionSet.add(d)
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):
VIDEO = 1
AUDIO = 2
SUBTITLE = 3
VIDEO = {'label': 'video', 'index': 1}
AUDIO = {'label': 'audio', 'index': 2}
SUBTITLE = {'label': 'subtitle', 'index': 3}
UNKNOWN = {'label': 'unknown', 'index': 0}
def label(self):
"""Returns the stream type as string"""
return str(self.value['label'])
labels = {
TrackType.VIDEO: "video",
TrackType.AUDIO: "audio",
TrackType.SUBTITLE: "subtitle"
}
def index(self):
"""Returns the stream type index"""
return int(self.value['index'])
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