52 Commits

Author SHA1 Message Date
6a03d4d6e2 nightly 2024-10-19 00:05:59 +02:00
a263c735aa nightly 2024-10-19 00:05:18 +02:00
5e0fdd6476 nightly 2024-10-18 22:09:39 +02:00
1cfa51f2ca Identify Show MWE 2024-10-18 21:29:58 +02:00
a3dc894ba7 ff 2024-10-18 21:28:11 +02:00
c0305ec0a7 ff 2024-10-18 21:23:21 +02:00
00ca7b92c1 ff 2024-10-18 21:21:09 +02:00
c4b3dd94f9 ff 2024-10-18 21:18:19 +02:00
5307eda92e ff 2024-10-18 21:14:58 +02:00
1620638110 ff 2024-10-18 20:53:04 +02:00
467977c81c df 2024-10-18 20:43:31 +02:00
fee7940660 ff 2024-10-18 20:36:10 +02:00
c03d4389ae ff 2024-10-18 20:34:13 +02:00
abdf13142e ff 2024-10-18 20:26:06 +02:00
5c96439fa8 ff 2024-10-18 20:22:16 +02:00
45a1c5aa4e ff 2024-10-18 20:12:56 +02:00
42f9486c64 ff 2024-10-18 20:04:19 +02:00
1eecd5a4f8 ff 2024-10-18 19:58:47 +02:00
c83f17dd44 ff 2024-10-18 19:54:13 +02:00
a01e8bfca5 alpha 0.0.1 2024-10-18 15:42:54 +02:00
cf49ff06d1 tmdb / filename / loop MWE 2024-10-18 12:35:25 +02:00
e3115e7557 merging tmdb und ffx complete crudest 2024-10-18 09:54:22 +02:00
ce2f3993e1 nightl 2024-10-17 22:54:29 +02:00
ee31634b0b inc ffx single descriptor 1/ 2024-10-17 15:58:22 +02:00
30d22892f8 season/episode recognition 2024-10-17 12:16:08 +02:00
Maveno
260c605201 UI edit audio layout 2024-10-17 11:16:20 +02:00
747ff41ad3 impl audio layout 2024-10-17 01:05:47 +02:00
fe1ed57758 nightl 2024-10-17 00:20:22 +02:00
a082058ce2 FfxController: metadata und disposition tokens krude 2024-10-16 21:27:13 +02:00
Maveno
9739f9aee4 inc 2024-10-16 15:56:58 +02:00
Maveno
5bb7dcc795 subindex into track order 2024-10-16 14:39:28 +02:00
ec3fb25c7b Inspect add edit pattern functionality 2024-10-16 12:05:20 +02:00
9fee9d1ae4 Inspect from Scratch -> RC 2024-10-16 11:53:17 +02:00
Maveno
e1cff6c8db inc 2024-10-16 10:07:54 +02:00
Maveno
93cc8a23c9 nightly 2024-10-13 21:37:28 +02:00
Maveno
bcb4e4d3d6 nightl update from file krude 2024-10-13 19:19:02 +02:00
Maveno
dba494b4e6 inc update diffs mwe 2024-10-13 14:36:03 +02:00
Maveno
ca57e981a6 combine track datatables 2024-10-13 11:46:56 +02:00
Maveno
ff93875a07 nightl: bugfixes und media diff inc 2024-10-12 20:42:07 +02:00
Maveno
bc3b593362 new file 2024-10-12 14:08:05 +02:00
Maveno
f920dec475 MediaDetailsScreen inc selected show 2024-10-12 13:42:44 +02:00
Maveno
84b1135861 inc MediaDetailsScreen UI 2024-10-12 11:54:32 +02:00
Maveno
aaa6b2cabc media/track comparison inc 2024-10-11 19:19:07 +02:00
Maveno
aea8c7e9ea simplify TrackDescriptors in MediaDescriptors 2024-10-10 07:56:16 +02:00
Maveno
b9aed1f0b6 md from pattern mwe 2024-10-09 21:55:50 +02:00
Maveno
12689fe61d nightl 2024-10-08 22:42:32 +02:00
Maveno
a280248f6a inc 2024-10-08 08:04:36 +02:00
Maveno
6b99091a55 show id per file properties mwe 2024-10-08 07:32:50 +02:00
Maveno
8ed6809ad0 inc 2024-10-06 17:46:42 +02:00
Maveno
882d021bb6 RFC imports 2024-10-06 16:24:01 +02:00
Maveno
131cca2c53 tracktags UI mwe 2024-10-06 16:07:23 +02:00
Maveno
a03449a32b add/edit tag to ui 2024-10-06 12:21:27 +02:00
33 changed files with 3425 additions and 1683 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
__pycache__ __pycache__
junk/ junk/
.vscode/launch.json

6
bin/dd.py Normal file
View File

@@ -0,0 +1,6 @@
from ffx.helper import dictDiff
a = {'name': 'yolo', 'mass': 56}
b = {'name': 'zolo', 'mass': 58}
print(dictDiff(a, b))

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,46 @@
import click
from enum import Enum from enum import Enum
from .track_type import TrackType from .track_type import TrackType
class AudioLayout(Enum): class AudioLayout(Enum):
LAYOUT_STEREO = {"layout": "stereo", "index": 1} LAYOUT_STEREO = {"label": "stereo", "index": 1}
LAYOUT_5_1 = {"layout": "5.1(side)", "index": 2} LAYOUT_5_1 = {"label": "5.1(side)", "index": 2}
LAYOUT_6_1 = {"layout": "6.1", "index": 3} LAYOUT_6_1 = {"label": "6.1", "index": 3}
LAYOUT_7_1 = {"layout": "7.1", "index": 4} #TODO: Does this exist? LAYOUT_7_1 = {"label": "7.1", "index": 4} #TODO: Does this exist?
LAYOUT_6CH = {"layout": "6ch", "index": 5} LAYOUT_6CH = {"label": "6ch", "index": 5}
LAYOUT_UNDEFINED = {"layout": "undefined", "index": 0} LAYOUT_UNDEFINED = {"label": "undefined", "index": 0}
def layout(self): def label(self):
"""Returns the layout as string""" """Returns the audio layout as string"""
return self.value['layout'] return str(self.value['label'])
def index(self): def index(self):
"""Returns the layout as string""" """Returns the audio layout as integer"""
return self.value['layout'] return int(self.value['index'])
@staticmethod
def fromLabel(label : str):
try:
return [a for a in AudioLayout if a.value['label'] == str(label)][0]
except:
raise click.ClickException('fromLabel failed')
return AudioLayout.LAYOUT_UNDEFINED
def identify(self, streamObj): @staticmethod
def fromIndex(index : int):
try:
return [a for a in AudioLayout if a.value['index'] == int(index)][0]
except:
raise click.ClickException('fromIndex failed')
return AudioLayout.LAYOUT_UNDEFINED
@staticmethod
def identify(streamObj):
FFPROBE_LAYOUT_KEY = 'channel_layout' FFPROBE_LAYOUT_KEY = 'channel_layout'
FFPROBE_CHANNELS_KEY = 'channels' FFPROBE_CHANNELS_KEY = 'channels'
@@ -34,7 +52,7 @@ class AudioLayout(Enum):
raise Exception('Not an ffprobe audio stream object') raise Exception('Not an ffprobe audio stream object')
if FFPROBE_LAYOUT_KEY in streamObj.keys(): if FFPROBE_LAYOUT_KEY in streamObj.keys():
matchingLayouts = [l for l in AudioLayout if l.value['layout'] == streamObj[FFPROBE_LAYOUT_KEY]] matchingLayouts = [l for l in AudioLayout if l.label() == streamObj[FFPROBE_LAYOUT_KEY]]
if matchingLayouts: if matchingLayouts:
return matchingLayouts[0] return matchingLayouts[0]

View File

@@ -1,23 +1,8 @@
import os, time, sqlite3, sqlalchemy from textual.app import App
from textual.app import App, ComposeResult
from textual.screen import Screen
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.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 .media_details_screen import MediaDetailsScreen
from .dashboard_screen import DashboardScreen
from .settings_screen import SettingsScreen
from .help_screen import HelpScreen
class FfxApp(App): class FfxApp(App):
@@ -37,9 +22,17 @@ class FfxApp(App):
def on_mount(self) -> None: def on_mount(self) -> None:
self.push_screen(ShowsScreen())
if 'command' in self.context.keys():
if self.context['command'] == 'shows':
self.push_screen(ShowsScreen())
if self.context['command'] == 'inspect':
self.push_screen(MediaDetailsScreen())
def getContext(self): def getContext(self):
"""Data 'output' method""" """Data 'output' method"""
return self.context return self.context

View File

@@ -1,2 +1,432 @@
import os, click, re
from ffx.media_descriptor import MediaDescriptor
from ffx.helper import DIFF_ADDED_KEY, DIFF_REMOVED_KEY, DIFF_CHANGED_KEY
from ffx.track_descriptor import TrackDescriptor
from ffx.model.track import Track
from ffx.audio_layout import AudioLayout
from ffx.track_type import TrackType
from ffx.video_encoder import VideoEncoder
from ffx.process import executeProcess
class FfxController(): class FfxController():
pass
COMMAND_TOKENS = ['ffmpeg', '-y']
NULL_TOKENS = ['-f', 'null', '/dev/null'] # -f null /dev/null
TEMP_FILE_NAME = "ffmpeg2pass-0.log"
DEFAULT_VIDEO_ENCODER = VideoEncoder.VP9.label()
DEFAULT_QUALITY = 23
DEFAULT_AV1_PRESET = 5
DEFAULT_FILE_FORMAT = 'webm'
DEFAULT_FILE_EXTENSION = 'webm'
DEFAULT_STEREO_BANDWIDTH = "128"
DEFAULT_AC3_BANDWIDTH = "256"
DEFAULT_DTS_BANDWIDTH = "320"
DEFAULT_CROP_START = 60
DEFAULT_CROP_LENGTH = 180
MKVMERGE_METADATA_KEYS = ['BPS',
'NUMBER_OF_FRAMES',
'NUMBER_OF_BYTES',
'_STATISTICS_WRITING_APP',
'_STATISTICS_WRITING_DATE_UTC',
'_STATISTICS_TAGS']
INPUT_FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm']
CHANNEL_MAP_5_1 = 'FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1'
def __init__(self,
context : dict,
targetMediaDescriptor : MediaDescriptor,
sourceMediaDescriptor : MediaDescriptor = None):
self.__context = context
self.__sourceMediaDescriptor = sourceMediaDescriptor
self.__targetMediaDescriptor = targetMediaDescriptor
def generateAV1Tokens(self, quality, preset, subIndex : int = 0):
return [f"-c:v:{int(subIndex)}", 'libsvtav1',
'-svtav1-params', f"crf={quality}:preset={preset}:tune=0:enable-overlays=1:scd=1:scm=0",
'-pix_fmt', 'yuv420p10le']
# -c:v:0 libvpx-vp9 -row-mt 1 -crf 32 -pass 1 -speed 4 -frame-parallel 0 -g 9999 -aq-mode 0
def generateVP9Pass1Tokens(self, quality, subIndex : int = 0):
return [f"-c:v:{int(subIndex)}",
'libvpx-vp9',
'-row-mt', '1',
'-crf', str(quality),
'-pass', '1',
'-speed', '4',
'-frame-parallel', '0',
'-g', '9999',
'-aq-mode', '0']
# -c:v:0 libvpx-vp9 -row-mt 1 -crf 32 -pass 2 -frame-parallel 0 -g 9999 -aq-mode 0 -auto-alt-ref 1 -lag-in-frames 25
def generateVP9Pass2Tokens(self, quality, subIndex : int = 0):
return [f"-c:v:{int(subIndex)}",
'libvpx-vp9',
'-row-mt', '1',
'-crf', str(quality),
'-pass', '2',
'-frame-parallel', '0',
'-g', '9999',
'-aq-mode', '0',
'-auto-alt-ref', '1',
'-lag-in-frames', '25']
def generateCropTokens(self):
if 'crop_start' in self.__context.keys() and 'crop_length' in self.__context.keys():
cropStart = int(self.__context['crop_start'])
cropLength = int(self.__context['crop_length'])
else:
cropStart = FfxController.DEFAULT_CROP_START
cropLength = FfxController.DEFAULT_CROP_LENGTH
return ['-ss', str(cropStart), '-t', str(cropLength)]
def generateDenoiseTokens(self, spatial=5, patch=7, research=7, hw=False):
filterName = 'nlmeans_opencl' if hw else 'nlmeans'
return ['-vf', f"{filterName}=s={spatial}:p={patch}:r={research}"]
def generateOutputTokens(self, filepath, format, ext):
return ['-f', format, f"{filepath}.{ext}"]
def generateAudioEncodingTokens(self):
"""Generates ffmpeg options audio streams including channel remapping, codec and bitrate"""
audioTokens = []
#sourceAudioTrackDescriptors = [smd for smd in self.__sourceMediaDescriptor.getAllTrackDescriptors() if smd.getType() == TrackType.AUDIO]
targetAudioTrackDescriptors = [rtd for rtd in self.__targetMediaDescriptor.getReorderedTrackDescriptors() if rtd.getType() == TrackType.AUDIO]
trackSubIndex = 0
for trackDescriptor in targetAudioTrackDescriptors:
# Calculate source sub index
#changedTargetTrackDescriptor : TrackDescriptor = targetAudioTrackDescriptors[trackDescriptor.getIndex()]
#changedTargetTrackSourceIndex = changedTargetTrackDescriptor.getSourceIndex()
#sourceSubIndex = sourceAudioTrackDescriptors[changedTargetTrackSourceIndex].getSubIndex()
trackAudioLayout = trackDescriptor.getAudioLayout()
#TODO: Sollte nicht die sub index unverändert bleiben wenn jellyfin reordering angewendet wurde?
# siehe auch: MediaDescriptor.getInputMappingTokens()
#trackSubIndex = trackDescriptor.getSubIndex()
if trackAudioLayout == AudioLayout.LAYOUT_6_1:
audioTokens += [f"-c:a:{trackSubIndex}",
'libopus',
f"-filter:a:{trackSubIndex}",
'channelmap=channel_layout=6.1',
f"-b:a:{trackSubIndex}",
self.__context['bitrates']['dts']]
if trackAudioLayout == AudioLayout.LAYOUT_5_1:
audioTokens += [f"-c:a:{trackSubIndex}",
'libopus',
f"-filter:a:{trackSubIndex}",
f"channelmap={FfxController.CHANNEL_MAP_5_1}",
f"-b:a:{trackSubIndex}",
self.__context['bitrates']['ac3']]
if trackAudioLayout == AudioLayout.LAYOUT_STEREO:
audioTokens += [f"-c:a:{trackSubIndex}",
'libopus',
f"-b:a:{trackSubIndex}",
self.__context['bitrates']['stereo']]
if trackAudioLayout == AudioLayout.LAYOUT_6CH:
audioTokens += [f"-c:a:{trackSubIndex}",
'libopus',
f"-filter:a:{trackSubIndex}",
f"channelmap={FfxController.CHANNEL_MAP_5_1}",
f"-b:a:{trackSubIndex}",
self.__context['bitrates']['ac3']]
trackSubIndex += 1
return audioTokens
def generateDispositionTokens(self):
"""Source media descriptor is optional"""
sourceTrackDescriptors = [] if self.__sourceMediaDescriptor is None else self.__sourceMediaDescriptor.getAllTrackDescriptors()
targetTrackDescriptors = self.__targetMediaDescriptor.getReorderedTrackDescriptors()
dispositionTokens = []
# for subStreamIndex in range(len(subDescriptor)):
for trackDescriptor in targetTrackDescriptors:
# Calculate source sub index. This applies only if a source media descriptor is defined.
if sourceTrackDescriptors:
changedTargetTrackDescriptor : TrackDescriptor = targetTrackDescriptors[trackDescriptor.getIndex()]
changedTargetTrackSourceIndex = changedTargetTrackDescriptor.getSourceIndex()
subIndex = sourceTrackDescriptors[changedTargetTrackSourceIndex].getSubIndex()
else:
subIndex = trackDescriptor.getSubIndex()
streamIndicator = trackDescriptor.getType().indicator()
dispositionSet = trackDescriptor.getDispositionSet()
if dispositionSet:
dispositionTokens += [f"-disposition:{streamIndicator}:{subIndex}", '+'.join([d.label() for d in dispositionSet])]
else:
dispositionTokens += [f"-disposition:{streamIndicator}:{subIndex}", '0']
return dispositionTokens
def generateMetadataTokens(self):
"""Source media descriptor is mandatory"""
mappingTokens = []
# click.echo(f"source media descriptor: track indices={[d.getIndex() for d in sourceMediaDescriptor.getAllTrackDescriptors()]}")
# click.echo(f"target media descriptor: track indices={[d.getIndex() for d in targetMediaDescriptor.getAllTrackDescriptors()]}")
# +jellyfin -jellyfin
mediaDifferences = self.__targetMediaDescriptor.compare(self.__sourceMediaDescriptor)
# media diff {'tracks': {'changed': {4: {'tags': {'added': {'Yolo'}}}}}}
click.echo(f"media diff {mediaDifferences}")
if MediaDescriptor.TAGS_KEY in mediaDifferences.keys():
sourceTags = self.__sourceMediaDescriptor.getTags()
targetTags = self.__targetMediaDescriptor.getTags()
if DIFF_REMOVED_KEY in mediaDifferences[MediaDescriptor.TAGS_KEY].keys():
# for removedTagKey in mediaDifferences[MediaDescriptor.TAGS_KEY][DIFF_REMOVED_KEY]:
# row = (f"removed media tag: key='{removedTagKey}' value='{sourceTags[removedTagKey]}'",)
# self.differencesTable.add_row(*map(str, row))
mappingTokens += [f"-map_metadata:g", "-1"]
for targetMediaTagKey in targetTags:
mappingTokens += [f"-metadata:g", f"{targetMediaTagKey}={targetTags[targetMediaTagKey]}"]
else:
if DIFF_ADDED_KEY in mediaDifferences[MediaDescriptor.TAGS_KEY].keys():
for addedTagKey in mediaDifferences[MediaDescriptor.TAGS_KEY][DIFF_ADDED_KEY]:
# row = (f"added media tag: key='{addedTagKey}' value='{targetTags[addedTagKey]}'",)
click.echo(f"added metadata key='{addedTagKey}' value='{targetTags[addedTagKey]}'->'{targetTags[addedTagKey]}'")
# self.differencesTable.add_row(*map(str, row))
#pass
mappingTokens += [f"-metadata:g", f"{addedTagKey}={targetTags[addedTagKey]}"]
if DIFF_CHANGED_KEY in mediaDifferences[MediaDescriptor.TAGS_KEY].keys():
for changedTagKey in mediaDifferences[MediaDescriptor.TAGS_KEY][DIFF_CHANGED_KEY]:
#row = (f"changed media tag: key='{changedTagKey}' value='{sourceTags[changedTagKey]}'->'{targetTags[changedTagKey]}'",)
click.echo(f"changed metadata key='{changedTagKey}' value='{sourceTags[changedTagKey]}'->'{targetTags[changedTagKey]}'")
# self.differencesTable.add_row(*map(str, row))
#pass
mappingTokens += [f"-metadata:g", f"{changedTagKey}={targetTags[changedTagKey]}"]
if MediaDescriptor.TRACKS_KEY in mediaDifferences.keys():
sourceTrackDescriptors = self.__sourceMediaDescriptor.getAllTrackDescriptors()
targetTrackDescriptors = self.__targetMediaDescriptor.getReorderedTrackDescriptors()
if DIFF_ADDED_KEY in mediaDifferences[MediaDescriptor.TRACKS_KEY].keys():
addedTracksIndices = mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_ADDED_KEY]
raise click.ClickException(f"FfxController.generateMetadataTokens(): Adding tracks is not supported. Track indices {addedTracksIndices}")
#raise click.ClickException(f"add track {mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_ADDED_KEY]}")
#for addedTrackIndex in mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_ADDED_KEY]:
#addedTrack : Track = targetTrackDescriptors[addedTrackIndex]
# row = (f"added {addedTrack.getType().label()} track: index={addedTrackIndex} lang={addedTrack.getLanguage().threeLetter()}",)
# self.differencesTable.add_row(*map(str, row))
if DIFF_REMOVED_KEY in mediaDifferences[MediaDescriptor.TRACKS_KEY].keys():
removedTracksIndices = mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_ADDED_KEY].keys()
raise click.ClickException(f"FfxController.generateMetadataTokens(): Removing tracks is not supported. Track indices {removedTracksIndices}")
#for removedTrackIndex in mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_REMOVED_KEY]:
# row = (f"removed track: index={removedTrackIndex}",)
# self.differencesTable.add_row(*map(str, row))
# media diff {'tracks': {'changed': {4: {'tags': {'added': {'Yolo'}}}}}}
if DIFF_CHANGED_KEY in mediaDifferences[MediaDescriptor.TRACKS_KEY].keys():
for changedTrackIndex in mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_CHANGED_KEY].keys():
changedTargetTrackDescriptor : TrackDescriptor = targetTrackDescriptors[changedTrackIndex]
changedTargetTrackSourceIndex = changedTargetTrackDescriptor.getSourceIndex()
changedTargetSourceSubIndex = sourceTrackDescriptors[changedTargetTrackSourceIndex].getSubIndex()
# changedSourceTrackDescriptor : TrackDescriptor = sourceTrackDescriptors[changedTargetTrackSourceIndex]
# changedSourceTrackSubIndex = changedSourceTrackDescriptor.getSubIndex()
changedTrackDiff : dict = mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_CHANGED_KEY][changedTrackIndex]
if MediaDescriptor.TAGS_KEY in changedTrackDiff.keys():
if DIFF_REMOVED_KEY in changedTrackDiff[MediaDescriptor.TAGS_KEY]:
#for removedTagKey in changedTrackDiff[MediaDescriptor.TAGS_KEY][DIFF_REMOVED_KEY]:
# row = (f"changed {changedTargetTrackDescriptor.getType().label()} track index={changedTrackIndex} removed key={removedTagKey}",)
# self.differencesTable.add_row(*map(str, row))
#addedTagValue = targetTrackDescriptors[changedTargetTrackSourceIndex].getTags()[addedTagKey]
mappingTokens += [f"-map_metadata:s:{changedTargetTrackDescriptor.getType().indicator()}:{changedTargetSourceSubIndex}", "-1"]
for targetTrackTagKey, targetTrackTagValue in changedTargetTrackDescriptor.getTags():
mappingTokens += [f"-metadata:s:{changedTargetTrackDescriptor.getType().indicator()}:{changedTargetSourceSubIndex}",
f"{targetTrackTagKey}={targetTrackTagValue}"]
else:
# media diff {'tracks': {'changed': {4: {'tags': {'added': {'Yolo'}}}}}}
if DIFF_ADDED_KEY in changedTrackDiff[MediaDescriptor.TAGS_KEY]:
for addedTagKey in changedTrackDiff[MediaDescriptor.TAGS_KEY][DIFF_ADDED_KEY]:
addedTagValue = targetTrackDescriptors[changedTargetTrackSourceIndex].getTags()[addedTagKey]
# addedTagValue = changedTrackDiff[MediaDescriptor.TAGS_KEY][DIFF_ADDED_KEY][addedTagKey]
# click.echo(f"addedTagValue={addedTagValue}")
# click.echo(f"sourceTrackDescriptors: subindex={[s.getSubIndex() for s in sourceTrackDescriptors]} sourceindex={[s.getSourceIndex() for s in sourceTrackDescriptors]} tags={[s.getTags() for s in sourceTrackDescriptors]}")
# click.echo(f"targetTrackDescriptors: subindex={[t.getSubIndex() for t in targetTrackDescriptors]} sourceindex={[t.getSourceIndex() for t in targetTrackDescriptors]} tags={[t.getTags() for t in targetTrackDescriptors]}")
# click.echo(f"changed track_index={changedTrackIndex} indicator={changedTargetTrackDescriptor.getType().indicator()} key={addedTagKey} value={addedTagValue} source_index={changedSourceTrackIndex}")
mappingTokens += [f"-metadata:s:{changedTargetTrackDescriptor.getType().indicator()}:{changedTargetSourceSubIndex}",
f"{addedTagKey}={addedTagValue}"]
# media diff {'tracks': {'changed': {4: {'tags': {'added': {'Yolo'}}}}}}
if DIFF_CHANGED_KEY in changedTrackDiff[MediaDescriptor.TAGS_KEY]:
for changedTagKey in changedTrackDiff[MediaDescriptor.TAGS_KEY][DIFF_CHANGED_KEY]:
changedTagValue = targetTrackDescriptors[changedTargetTrackSourceIndex].getTags()[changedTagKey]
# sourceSubIndex = sourceTrackDescriptors[changedTargetTrackSourceIndex].getSubIndex()
# addedTagValue = changedTrackDiff[MediaDescriptor.TAGS_KEY][DIFF_ADDED_KEY][addedTagKey]
# click.echo(f"addedTagValue={addedTagValue}")
# click.echo(f"sourceTrackDescriptors: subindex={[s.getSubIndex() for s in sourceTrackDescriptors]} sourceindex={[s.getSourceIndex() for s in sourceTrackDescriptors]} tags={[s.getTags() for s in sourceTrackDescriptors]}")
# click.echo(f"targetTrackDescriptors: subindex={[t.getSubIndex() for t in targetTrackDescriptors]} sourceindex={[t.getSourceIndex() for t in targetTrackDescriptors]} tags={[t.getTags() for t in targetTrackDescriptors]}")
# click.echo(f"changed track_index={changedTrackIndex} indicator={changedTargetTrackDescriptor.getType().indicator()} key={addedTagKey} value={addedTagValue} source_index={changedSourceTrackIndex}")
mappingTokens += [f"-metadata:s:{changedTargetTrackDescriptor.getType().indicator()}:{changedTargetSourceSubIndex}",
f"{changedTagKey}={changedTagValue}"]
# if TrackDescriptor.DISPOSITION_SET_KEY in changedTrackDiff.keys():
# if DIFF_ADDED_KEY in changedTrackDiff[TrackDescriptor.DISPOSITION_SET_KEY]:
# for addedDisposition in changedTrackDiff[TrackDescriptor.DISPOSITION_SET_KEY][DIFF_ADDED_KEY]:
# # row = (f"changed {changedTargetTrackDescriptor.getType().label()} track index={changedTrackIndex} added disposition={addedDisposition.label()}",)
# # self.differencesTable.add_row(*map(str, row))
# pass
# if DIFF_REMOVED_KEY in changedTrackDiff[TrackDescriptor.DISPOSITION_SET_KEY]:
# for removedDisposition in changedTrackDiff[TrackDescriptor.DISPOSITION_SET_KEY][DIFF_REMOVED_KEY]:
# # row = (f"changed {changedTargetTrackDescriptor.getType().label()} track index={changedTrackIndex} removed disposition={removedDisposition.label()}",)
# # self.differencesTable.add_row(*map(str, row))
# pass
return mappingTokens
def runJob(self,
sourcePath,
targetPath,
videoEncoder: VideoEncoder = VideoEncoder.VP9,
quality: int = DEFAULT_QUALITY,
preset: int = DEFAULT_AV1_PRESET,
denoise: bool = False):
commandTokens = FfxController.COMMAND_TOKENS + ['-i', sourcePath]
if videoEncoder == VideoEncoder.AV1:
commandSequence = (commandTokens
+ self.__targetMediaDescriptor.getImportFileTokens()
+ self.__targetMediaDescriptor.getInputMappingTokens()
+ self.generateDispositionTokens())
if not self.__sourceMediaDescriptor is None:
commandSequence += self.generateMetadataTokens()
if denoise:
commandSequence += self.generateDenoiseTokens()
commandSequence += (self.generateAudioEncodingTokens()
+ self.generateAV1Tokens(int(quality), int(preset))
+ self.generateAudioEncodingTokens())
if self.__context['perform_crop']:
commandSequence += FfxController.generateCropTokens()
commandSequence += self.generateOutputTokens(targetPath,
FfxController.DEFAULT_FILE_FORMAT,
FfxController.DEFAULT_FILE_EXTENSION)
click.echo(f"Command: {' '.join(commandSequence)}")
if not self.__context['dry_run']:
executeProcess(commandSequence)
if videoEncoder == VideoEncoder.VP9:
commandSequence1 = (commandTokens
+ self.__targetMediaDescriptor.getInputMappingTokens()
+ self.generateVP9Pass1Tokens(int(quality)))
if self.__context['perform_crop']:
commandSequence1 += FfxController.generateCropTokens()
commandSequence1 += FfxController.NULL_TOKENS
click.echo(f"Command 1: {' '.join(commandSequence1)}")
if os.path.exists(FfxController.TEMP_FILE_NAME):
os.remove(FfxController.TEMP_FILE_NAME)
if not self.__context['dry_run']:
executeProcess(commandSequence1)
commandSequence2 = (commandTokens
+ self.__targetMediaDescriptor.getImportFileTokens()
+ self.__targetMediaDescriptor.getInputMappingTokens()
+ self.generateDispositionTokens())
if not self.__sourceMediaDescriptor is None:
commandSequence2 += self.generateMetadataTokens()
if denoise:
commandSequence2 += self.generateDenoiseTokens()
commandSequence2 += self.generateVP9Pass2Tokens(int(quality)) + self.generateAudioEncodingTokens()
if self.__context['perform_crop']:
commandSequence2 += FfxController.generateCropTokens()
commandSequence2 += self.generateOutputTokens(targetPath,
FfxController.DEFAULT_FILE_FORMAT,
FfxController.DEFAULT_FILE_EXTENSION)
click.echo(f"Command 2: {' '.join(commandSequence2)}")
if not self.__context['dry_run']:
executeProcess(commandSequence2)

View File

@@ -3,13 +3,12 @@ import os, re, click, json
from .media_descriptor import MediaDescriptor from .media_descriptor import MediaDescriptor
from .pattern_controller import PatternController from .pattern_controller import PatternController
#from .track_type import TrackType
#from .audio_layout import AudioLayout
#from .track_disposition import TrackDisposition
from .process import executeProcess from .process import executeProcess
from ffx.model.pattern import Pattern
from ffx.ffx_controller import FfxController
from ffx.show_descriptor import ShowDescriptor
class FileProperties(): class FileProperties():
@@ -18,9 +17,12 @@ 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]+)'
DEFAULT_INDEX_DIGITS = 3
def __init__(self, context, sourcePath): def __init__(self, context, sourcePath):
self.context = context
# Separate basedir, basename and extension for current source file # Separate basedir, basename and extension for current source file
self.__sourcePath = sourcePath self.__sourcePath = sourcePath
@@ -37,51 +39,26 @@ class FileProperties():
self.__sourceFilenameExtension = '' self.__sourceFilenameExtension = ''
se_match = re.compile(FileProperties.SEASON_EPISODE_INDICATOR_MATCH) self.__pc = PatternController(context)
e_match = re.compile(FileProperties.EPISODE_INDICATOR_MATCH)
se_result = se_match.search(self.__sourceFilename) matchResult = self.__pc.matchFilename(self.__sourceFilename)
e_result = e_match.search(self.__sourceFilename)
self.__pattern: Pattern = matchResult['pattern'] if matchResult else None
matchedGroups = matchResult['match'].groups() if matchResult else {}
seIndicator = matchedGroups[0] if matchedGroups else self.__sourceFilename
se_match = re.search(FileProperties.SEASON_EPISODE_INDICATOR_MATCH, seIndicator)
e_match = re.search(FileProperties.EPISODE_INDICATOR_MATCH, seIndicator)
self.__season = -1 self.__season = -1
self.__episode = -1 self.__episode = -1
file_index = 0
if se_result is not None: if se_match is not None:
self.__season = int(se_result.group(1)) self.__season = int(se_match.group(1))
self.__episode = int(se_result.group(2)) self.__episode = int(se_match.group(2))
elif e_result is not None: elif e_match is not None:
self.__episode = int(e_result.group(1)) self.__episode = int(e_match.group(1))
else:
file_index += 1
pc = PatternController(context)
pattern = pc.matchFilename(self.__sourceFilename)
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]
#
def getFormatData(self): def getFormatData(self):
@@ -189,32 +166,66 @@ class FileProperties():
return json.loads(ffprobeOutput)['streams'] 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): def getMediaDescriptor(self):
return MediaDescriptor.fromFfprobe(self.getFormatData(), self.getStreamData()) return MediaDescriptor.fromFfprobe(self.getFormatData(), self.getStreamData())
# formatData = self.getFormatData()
# streamData = self.getStreamData()
def getShowId(self) -> int:
"""Result is -1 if the filename did not match anything in database"""
return self.__pattern.getShowId() if self.__pattern is not None else -1
def getPattern(self) -> Pattern:
"""Result is None if the filename did not match anything in database"""
return self.__pattern
def getSeason(self):
return int(self.__season)
def getEpisode(self):
return int(self.__episode)
def assembleTargetFileBasename(self,
label: str = "",
quality: int = -1,
fileIndex: int = -1,
indexDigits: int = DEFAULT_INDEX_DIGITS,
extraTokens: list = []):
if 'show_descriptor' in self.context.keys():
season_digits = self.context['show_descriptor'][ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY]
episode_digits = self.context['show_descriptor'][ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY]
else:
season_digits = ShowDescriptor.DEFAULT_INDICATOR_SEASON_DIGITS
episode_digits = ShowDescriptor.DEFAULT_INDICATOR_EPISODE_DIGITS
targetFilenameTokens = []
# targetFilenameExtension = FfxController.DEFAULT_FILE_EXTENSION if extension is None else str(extension)
if not label:
targetFilenameTokens = [self.__sourceFileBasename]
else:
targetFilenameTokens = [label]
if fileIndex > -1:
targetFilenameTokens += [f"{fileIndex:0{indexDigits}d}"]
elif self.__season > -1 and self.__episode > -1:
targetFilenameTokens += [f"S{self.__season:0{season_digits}d}E{self.__episode:0{episode_digits}d}"]
elif self.__episode > -1:
targetFilenameTokens += [f"E{self.__episode:0{episode_digits}d}"]
if quality != -1:
targetFilenameTokens += [f"q{quality}"]
# In case source and target filenames are the same add an extension to distinct output from input
#if not label and self.__sourceFilenameExtension == targetFilenameExtension:
# targetFilenameTokens += ['ffx']
targetFilenameTokens += extraTokens
targetFilename = '_'.join(targetFilenameTokens)
click.echo(f"Target filename: {targetFilename}")
return targetFilename

47
bin/ffx/helper.py Normal file
View File

@@ -0,0 +1,47 @@
DIFF_ADDED_KEY = 'added'
DIFF_REMOVED_KEY = 'removed'
DIFF_CHANGED_KEY = 'changed'
DIFF_UNCHANGED_KEY = 'unchanged'
def dictDiff(a : dict, b : dict):
a_keys = set(a.keys())
b_keys = set(b.keys())
a_only = a_keys - b_keys
b_only = b_keys - a_keys
a_b = a_keys & b_keys
changed = {k for k in a_b if a[k] != b[k]}
diffResult = {}
if a_only:
diffResult[DIFF_REMOVED_KEY] = a_only
diffResult[DIFF_UNCHANGED_KEY] = b_keys
if b_only:
diffResult[DIFF_ADDED_KEY] = b_only
if changed:
diffResult[DIFF_CHANGED_KEY] = changed
return diffResult
def setDiff(a : set, b : set) -> set:
a_only = a - b
b_only = b - a
diffResult = {}
if a_only:
diffResult[DIFF_REMOVED_KEY] = a_only
if b_only:
diffResult[DIFF_ADDED_KEY] = b_only
return diffResult
def filterFilename(fileName: str) -> str:
fileName = str(fileName).replace(':', ';')
return fileName

View File

@@ -93,15 +93,6 @@ class IsoLanguage(Enum):
return foundLangs[0] if foundLangs else IsoLanguage.UNDEFINED return foundLangs[0] if foundLangs else IsoLanguage.UNDEFINED
# def get(lang : str):
#
# selectedLangs = [l for l in IsoLanguage if l.value['iso639_2'] == lang]
#
# if selectedLangs:
# return selectedLangs[0]
# else:
# return None
def label(self): def label(self):
return str(self.value['name']) return str(self.value['name'])

View File

@@ -1,43 +1,479 @@
import os
import re
import click
from typing import List, Self
from ffx.track_type import TrackType from ffx.track_type import TrackType
from ffx.track_disposition import TrackDisposition
from ffx.track_descriptor import TrackDescriptor from ffx.track_descriptor import TrackDescriptor
from ffx.helper import dictDiff, DIFF_ADDED_KEY, DIFF_CHANGED_KEY, DIFF_REMOVED_KEY
class MediaDescriptor():
class MediaDescriptor:
"""This class represents the structural content of a media file including streams and metadata""" """This class represents the structural content of a media file including streams and metadata"""
CONTEXT_KEY = "context"
TAGS_KEY = "tags"
TRACKS_KEY = "tracks"
TRACK_DESCRIPTOR_LIST_KEY = "track_descriptors"
CLEAR_TAGS_FLAG_KEY = "clear_tags"
FFPROBE_DISPOSITION_KEY = "disposition"
FFPROBE_TAGS_KEY = "tags"
FFPROBE_CODEC_TYPE_KEY = "codec_type"
JELLYFIN_ORDER_FLAG_KEY = "jellyfin_order"
EXCLUDED_MEDIA_TAGS = ["creation_time"]
SEASON_EPISODE_STREAM_LANGUAGE_MATCH = '[sS]([0-9]+)[eE]([0-9]+)_([0-9]+)_([a-z]{3})'
SUBTITLE_FILE_EXTENSION = 'vtt'
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.__mediaTags = kwargs['tags'] if 'tags' in kwargs.keys() else {} if MediaDescriptor.TAGS_KEY in kwargs.keys():
self.__trackDescriptors = kwargs['trackDescriptors'] if 'trackDescriptors' in kwargs.keys() else {} if type(kwargs[MediaDescriptor.TAGS_KEY]) is not dict:
self.__clearTags = kwargs['clearTags'] if 'clearTags' in kwargs.keys() else False raise TypeError(
f"MediaDescriptor.__init__(): Argument {MediaDescriptor.TAGS_KEY} is required to be of type dict"
)
self.__mediaTags = kwargs[MediaDescriptor.TAGS_KEY]
else:
self.__mediaTags = {}
if MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY in kwargs.keys():
if (
type(kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY]) is not list
): # Use List typehint for TrackDescriptor as well if it works
raise TypeError(
f"MediaDescriptor.__init__(): Argument {MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY} is required to be of type list"
)
for d in kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY]:
if type(d) is not TrackDescriptor:
raise TypeError(
f"TrackDesciptor.__init__(): All elements of argument list {MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY} are required to be of type TrackDescriptor"
)
self.__trackDescriptors = kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY]
else:
self.__trackDescriptors = []
if MediaDescriptor.CLEAR_TAGS_FLAG_KEY in kwargs.keys():
if type(kwargs[MediaDescriptor.CLEAR_TAGS_FLAG_KEY]) is not bool:
raise TypeError(
f"MediaDescriptor.__init__(): Argument {MediaDescriptor.CLEAR_TAGS_FLAG_KEY} is required to be of type bool"
)
self.__clearTags = kwargs[MediaDescriptor.CLEAR_TAGS_FLAG_KEY]
else:
self.__clearTags = False
if MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY in kwargs.keys():
if type(kwargs[MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY]) is not bool:
raise TypeError(
f"MediaDescriptor.__init__(): Argument {MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY} is required to be of type bool"
)
self.__jellyfinOrder = kwargs[MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY]
else:
self.__jellyfinOrder = False
def getDefaultVideoTrack(self):
videoDefaultTracks = [
v
for v in self.getVideoTracks()
if TrackDisposition.DEFAULT in v.getDispositionSet()
]
if len(videoDefaultTracks) > 1:
raise ValueError(
"MediaDescriptor.getDefaultVideoTrack(): More than one default video track is not supported"
)
return videoDefaultTracks[0] if videoDefaultTracks else None
def getForcedVideoTrack(self):
videoForcedTracks = [
v
for v in self.getVideoTracks()
if TrackDisposition.FORCED in v.getDispositionSet()
]
if len(videoForcedTracks) > 1:
raise ValueError(
"MediaDescriptor.getForcedVideoTrack(): More than one forced video track is not supported"
)
return videoForcedTracks[0] if videoForcedTracks else None
def getDefaultAudioTrack(self):
audioDefaultTracks = [
a
for a in self.getAudioTracks()
if TrackDisposition.DEFAULT in a.getDispositionSet()
]
if len(audioDefaultTracks) > 1:
raise ValueError(
"MediaDescriptor.getDefaultAudioTrack(): More than one default audio track is not supported"
)
return audioDefaultTracks[0] if audioDefaultTracks else None
def getForcedAudioTrack(self):
audioForcedTracks = [
a
for a in self.getAudioTracks()
if TrackDisposition.FORCED in a.getDispositionSet()
]
if len(audioForcedTracks) > 1:
raise ValueError(
"MediaDescriptor.getForcedAudioTrack(): More than one forced audio track is not supported"
)
return audioForcedTracks[0] if audioForcedTracks else None
def getDefaultSubtitleTrack(self):
subtitleDefaultTracks = [
s
for s in self.getSubtitleTracks()
if TrackDisposition.DEFAULT in s.getDispositionSet()
]
if len(subtitleDefaultTracks) > 1:
raise ValueError(
"MediaDescriptor.getDefaultSubtitleTrack(): More than one default subtitle track is not supported"
)
return subtitleDefaultTracks[0] if subtitleDefaultTracks else None
def getForcedSubtitleTrack(self):
subtitleForcedTracks = [
s
for s in self.getSubtitleTracks()
if TrackDisposition.FORCED in s.getDispositionSet()
]
if len(subtitleForcedTracks) > 1:
raise ValueError(
"MediaDescriptor.getForcedSubtitleTrack(): More than one forced subtitle track is not supported"
)
return subtitleForcedTracks[0] if subtitleForcedTracks else None
def setDefaultSubTrack(self, trackType: TrackType, subIndex: int):
for t in self.getAllTrackDescriptors():
if t.getType() == trackType:
t.setDispositionFlag(
TrackDisposition.DEFAULT, t.getSubIndex() == int(subIndex)
)
def setForcedSubTrack(self, trackType: TrackType, subIndex: int):
for t in self.getAllTrackDescriptors():
if t.getType() == trackType:
t.setDispositionFlag(
TrackDisposition.FORCED, t.getSubIndex() == int(subIndex)
)
def checkDefaultAndForcedDispositions(self):
try:
self.getDefaultVideoTrack()
self.getForcedVideoTrack()
self.getDefaultAudioTrack()
self.getForcedAudioTrack()
self.getDefaultSubtitleTrack()
self.getForcedSubtitleTrack()
return True
except ValueError:
return False
def getReorderedTrackDescriptors(self):
videoTracks = self.sortSubIndices(self.getVideoTracks())
audioTracks = self.sortSubIndices(self.getAudioTracks())
subtitleTracks = self.sortSubIndices(self.getSubtitleTracks())
videoDefaultTrack = self.getDefaultVideoTrack()
self.getForcedVideoTrack()
audioDefaultTrack = self.getDefaultAudioTrack()
self.getForcedAudioTrack()
subtitleDefaultTrack = self.getDefaultSubtitleTrack()
self.getForcedSubtitleTrack()
if self.__jellyfinOrder:
if not videoDefaultTrack is None:
videoTracks.append(
videoTracks.pop(videoTracks.index(videoDefaultTrack))
)
if not audioDefaultTrack is None:
audioTracks.append(
audioTracks.pop(audioTracks.index(audioDefaultTrack))
)
if not subtitleDefaultTrack is None:
subtitleTracks.append(
subtitleTracks.pop(subtitleTracks.index(subtitleDefaultTrack))
)
reorderedTrackDescriptors = videoTracks + audioTracks + subtitleTracks
orderedSourceTrackSequence = [
t.getSourceIndex() for t in reorderedTrackDescriptors
]
if len(set(orderedSourceTrackSequence)) < len(orderedSourceTrackSequence):
raise ValueError(
f"Multiple streams originating from the same source stream not supported"
)
return reorderedTrackDescriptors
@classmethod @classmethod
def fromFfprobe(cls, formatData, streamData): def fromFfprobe(cls, formatData, streamData):
descriptors = {} kwargs = {}
if MediaDescriptor.FFPROBE_TAGS_KEY in formatData.keys():
kwargs[MediaDescriptor.TAGS_KEY] = formatData[
MediaDescriptor.FFPROBE_TAGS_KEY
]
kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY] = []
# TODO: Evtl obsolet
subIndexCounters = {}
for streamObj in streamData: for streamObj in streamData:
trackType = TrackType.fromLabel(streamObj['codec_type']) ffprobeCodecType = streamObj[MediaDescriptor.FFPROBE_CODEC_TYPE_KEY]
trackType = TrackType.fromLabel(ffprobeCodecType)
if trackType != TrackType.UNKNOWN: if trackType != TrackType.UNKNOWN:
if trackType.label() not in descriptors.keys(): if trackType not in subIndexCounters.keys():
descriptors[trackType.label()] = [] subIndexCounters[trackType] = 0
descriptors[trackType.label()].append(TrackDescriptor.fromFfprobe(streamObj)) kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY].append(
TrackDescriptor.fromFfprobe(
return cls(tags=formatData['tags'] if 'tags' in formatData.keys() else {}, streamObj, subIndex=subIndexCounters[trackType]
trackDescriptors = descriptors) )
)
subIndexCounters[trackType] += 1
return cls(**kwargs)
def getTags(self): def getTags(self):
return self.__mediaTags return self.__mediaTags
def sortSubIndices(
self, descriptors: List[TrackDescriptor]
) -> List[TrackDescriptor]:
subIndex = 0
for t in descriptors:
t.setSubIndex(subIndex)
subIndex += 1
return descriptors
def getAudioTracks(self): def getAllTrackDescriptors(self) -> List[TrackDescriptor]:
return self.__trackDescriptors[TrackType.AUDIO.label()] if TrackType.AUDIO.label() in self.__trackDescriptors.keys() else [] return self.getVideoTracks() + self.getAudioTracks() + self.getSubtitleTracks()
def getSubtitleTracks(self): def getVideoTracks(self) -> List[TrackDescriptor]:
return self.__trackDescriptors[TrackType.SUBTITLE.label()] if TrackType.SUBTITLE.label() in self.__trackDescriptors.keys() else [] return [
v for v in self.__trackDescriptors.copy() if v.getType() == TrackType.VIDEO
]
def getAudioTracks(self) -> List[TrackDescriptor]:
return [
a for a in self.__trackDescriptors.copy() if a.getType() == TrackType.AUDIO
]
def getSubtitleTracks(self) -> List[TrackDescriptor]:
return [
s
for s in self.__trackDescriptors.copy()
if s.getType() == TrackType.SUBTITLE
]
def getJellyfin(self):
return self.__jellyfinOrder
def setJellyfinOrder(self, state):
self.__jellyfinOrder = state
def getClearTags(self):
return self.__clearTags
def compare(self, vsMediaDescriptor: Self):
if not isinstance(vsMediaDescriptor, self.__class__):
raise click.ClickException(
f"MediaDescriptor.compare(): Argument is required to be of type {self.__class__}"
)
vsTags = vsMediaDescriptor.getTags()
tags = self.getTags()
# HINT: Some tags differ per file, for example creation_time, so these are removed before diff
for emt in MediaDescriptor.EXCLUDED_MEDIA_TAGS:
if emt in tags.keys():
del tags[emt]
if emt in vsTags.keys():
del vsTags[emt]
tagsDiff = dictDiff(vsTags, tags)
compareResult = {}
if tagsDiff:
compareResult[MediaDescriptor.TAGS_KEY] = tagsDiff
# Target track configuration (from DB)
# tracks = self.getAllTrackDescriptors()
tracks = self.getReorderedTrackDescriptors()
numTracks = len(tracks)
# Current track configuration (of file)
vsTracks = vsMediaDescriptor.getAllTrackDescriptors()
numVsTracks = len(vsTracks)
maxNumOfTracks = max(numVsTracks, numTracks)
trackCompareResult = {}
for tp in range(maxNumOfTracks):
# inspect/update funktionier nur so
if self.__jellyfinOrder:
vsTrackIndex = tracks[tp].getSourceIndex()
else:
vsTrackIndex = tp
# vsTrackIndex = tracks[tp].getSourceIndex()
# Will trigger if tracks are missing in file
if tp > (numVsTracks - 1):
if DIFF_ADDED_KEY not in trackCompareResult.keys():
trackCompareResult[DIFF_ADDED_KEY] = set()
trackCompareResult[DIFF_ADDED_KEY].add(tracks[tp].getIndex())
continue
# Will trigger if tracks are missing in DB definition
# New tracks will be added per update via this way
if tp > (numTracks - 1):
if DIFF_REMOVED_KEY not in trackCompareResult.keys():
trackCompareResult[DIFF_REMOVED_KEY] = {}
trackCompareResult[DIFF_REMOVED_KEY][
vsTracks[vsTrackIndex].getIndex()
] = vsTracks[vsTrackIndex]
continue
# assumption is made here that the track order will not change for all files of a sequence
trackDiff = tracks[tp].compare(vsTracks[vsTrackIndex])
if trackDiff:
if DIFF_CHANGED_KEY not in trackCompareResult.keys():
trackCompareResult[DIFF_CHANGED_KEY] = {}
trackCompareResult[DIFF_CHANGED_KEY][
vsTracks[vsTrackIndex].getIndex()
] = trackDiff
if trackCompareResult:
compareResult[MediaDescriptor.TRACKS_KEY] = trackCompareResult
return compareResult
def getImportFileTokens(self, use_sub_index: bool = True):
reorderedTrackDescriptors = self.getReorderedTrackDescriptors()
importFileTokens = []
for rtd in reorderedTrackDescriptors:
importedFilePath = rtd.getExternalSourceFilePath()
if not importedFilePath is None:
importFileTokens += [
"-i",
importedFilePath,
]
return importFileTokens
def getInputMappingTokens(self, use_sub_index: bool = True):
reorderedTrackDescriptors = self.getReorderedTrackDescriptors()
inputMappingTokens = []
filePointer = 1
for rtd in reorderedTrackDescriptors:
importedFilePath = rtd.getExternalSourceFilePath()
trackType = rtd.getType()
if use_sub_index:
if importedFilePath is None:
inputMappingTokens += [
"-map",
f"0:{trackType.indicator()}:{rtd.getSubIndex()}",
]
else:
inputMappingTokens += [
"-map",
f"{filePointer}:{trackType.indicator()}:0",
]
filePointer += 1
else:
inputMappingTokens += ["-map", f"0:{rtd.getIndex()}"]
return inputMappingTokens
def searchSubtitleFiles(searchDirectory, prefix):
sesl_match = re.compile(MediaDescriptor.SEASON_EPISODE_STREAM_LANGUAGE_MATCH)
availableFileSubtitleDescriptors = []
for subtitleFilename in os.listdir(searchDirectory):
if subtitleFilename.startswith(prefix) and subtitleFilename.endswith(
"." + MediaDescriptor.SUBTITLE_FILE_EXTENSION
):
sesl_result = sesl_match.search(subtitleFilename)
if sesl_result is not None:
subtitleFilePath = os.path.join(searchDirectory, subtitleFilename)
if os.path.isfile(subtitleFilePath):
subtitleFileDescriptor = {}
subtitleFileDescriptor["path"] = subtitleFilePath
subtitleFileDescriptor["season"] = int(sesl_result.group(1))
subtitleFileDescriptor["episode"] = int(sesl_result.group(2))
subtitleFileDescriptor["stream"] = int(sesl_result.group(3))
subtitleFileDescriptor["language"] = sesl_result.group(4)
availableFileSubtitleDescriptors.append(subtitleFileDescriptor)
click.echo(
f"Found {len(availableFileSubtitleDescriptors)} subtitles in files\n"
)
return availableFileSubtitleDescriptors
def importSubtitles(
self, searchDirectory, prefix, season: int = -1, episode: int = -1
):
availableFileSubtitleDescriptors = self.searchSubtitleFiles(
searchDirectory, prefix
)
subtitleTracks = self.getSubtitleTracks()
# if len(availableFileSubtitleDescriptors) != len(subtitleTracks):
# raise click.ClickException(f"MediaDescriptor.importSubtitles(): Number if subtitle files not matching number of subtitle tracks")
matchingFileSubtitleDescriptors = (
sorted(
[
d
for d in availableFileSubtitleDescriptors
if d["season"] == int(season) and d["episode"] == int(episode)
],
key=lambda d: d["stream"],
)
if availableFileSubtitleDescriptors
else []
)
for mfsd in matchingFileSubtitleDescriptors:
matchingSubtitleTrackDescriptor = [
s for s in subtitleTracks if s.getIndex() == mfsd["stream"]
]
if matchingSubtitleTrackDescriptor:
matchingSubtitleTrackDescriptor[0].setExternalSourceFilePath(
mfsd["path"]
)

View File

@@ -0,0 +1,638 @@
import os, click, re
from textual import events
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import Header, Footer, Static, Button, Input, DataTable
from textual.containers import Grid
from ffx.model.show import Show
from ffx.model.pattern import Pattern
from .pattern_controller import PatternController
from .show_controller import ShowController
from .track_controller import TrackController
from .tag_controller import TagController
from .track_details_screen import TrackDetailsScreen
from .track_delete_screen import TrackDeleteScreen
from .show_details_screen import ShowDetailsScreen
from .pattern_details_screen import PatternDetailsScreen
from ffx.track_type import TrackType
from ffx.model.track import Track
from ffx.track_disposition import TrackDisposition
from ffx.track_descriptor import TrackDescriptor
from ffx.show_descriptor import ShowDescriptor
from textual.widgets._data_table import CellDoesNotExist
from ffx.media_descriptor import MediaDescriptor
from ffx.file_properties import FileProperties
from ffx.helper import DIFF_ADDED_KEY, DIFF_CHANGED_KEY, DIFF_REMOVED_KEY
# Screen[dict[int, str, int]]
class MediaDetailsScreen(Screen):
CSS = """
Grid {
grid-size: 5 8;
grid-rows: 8 2 2 2 8 2 2 8;
grid-columns: 25 25 100 10 75;
height: 100%;
width: 100%;
padding: 1;
}
Input {
border: none;
}
Button {
border: none;
}
DataTable {
min-height: 40;
}
#toplabel {
height: 1;
}
.two {
column-span: 2;
}
.three {
column-span: 3;
}
.four {
column-span: 4;
}
.five {
column-span: 5;
}
.triple {
row-span: 3;
}
.box {
height: 100%;
border: solid green;
}
.yellow {
tint: yellow 40%;
}
#differences-table {
row-span: 8;
/* tint: magenta 40%; */
}
/* #pattern_input {
tint: red 40%;
}*/
"""
BINDINGS = [
("n", "new_pattern", "New Pattern"),
("u", "update_pattern", "Update Pattern"),
("e", "edit_pattern", "Edit Pattern"),
]
def __init__(self):
super().__init__()
self.context = self.app.getContext()
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.__tac = TagController(context = self.context)
if not 'command' in self.context.keys() or self.context['command'] != 'inspect':
raise click.ClickException(f"MediaDetailsScreen.__init__(): Can only perform command 'inspect'")
if not 'arguments' in self.context.keys() or not 'filename' in self.context['arguments'].keys() or not self.context['arguments']['filename']:
raise click.ClickException(f"MediaDetailsScreen.__init__(): Argument 'filename' is required to be provided for command 'inspect'")
self.__mediaFilename = self.context['arguments']['filename']
if not os.path.isfile(self.__mediaFilename):
raise click.ClickException(f"MediaDetailsScreen.__init__(): Media file {self.__mediaFilename} does not exist")
self.loadProperties()
def getRowIndexFromShowId(self, showId : int) -> int:
"""Find the index of the row where the value in the specified column matches the target_value."""
for rowKey, row in self.showsTable.rows.items(): # dict[RowKey, Row]
rowData = self.showsTable.get_row(rowKey)
try:
if showId == int(rowData[0]):
return int(self.showsTable.get_row_index(rowKey))
except:
continue
return None
def loadProperties(self):
self.__mediaFileProperties = FileProperties(self.context, self.__mediaFilename)
self.__currentMediaDescriptor = self.__mediaFileProperties.getMediaDescriptor()
#HINT: This is None if the filename did not match anything in database
self.__currentPattern = self.__mediaFileProperties.getPattern()
self.__targetMediaDescriptor = self.__currentPattern.getMediaDescriptor() if self.__currentPattern is not None else None
# Enumerating differences between media descriptors
# from file (=current) vs from stored in database (=target)
try:
self.__mediaDifferences = self.__targetMediaDescriptor.compare(self.__currentMediaDescriptor) if self.__currentPattern is not None else {}
except ValueError:
self.__mediaDifferences = {}
def updateDifferences(self):
self.loadProperties()
self.differencesTable.clear()
if MediaDescriptor.TAGS_KEY in self.__mediaDifferences.keys():
currentTags = self.__currentMediaDescriptor.getTags()
targetTags = self.__targetMediaDescriptor.getTags()
if DIFF_ADDED_KEY in self.__mediaDifferences[MediaDescriptor.TAGS_KEY].keys():
for addedTagKey in self.__mediaDifferences[MediaDescriptor.TAGS_KEY][DIFF_ADDED_KEY]:
row = (f"added media tag: key='{addedTagKey}' value='{targetTags[addedTagKey]}'",)
self.differencesTable.add_row(*map(str, row))
if DIFF_REMOVED_KEY in self.__mediaDifferences[MediaDescriptor.TAGS_KEY].keys():
for removedTagKey in self.__mediaDifferences[MediaDescriptor.TAGS_KEY][DIFF_REMOVED_KEY]:
row = (f"removed media tag: key='{removedTagKey}' value='{currentTags[removedTagKey]}'",)
self.differencesTable.add_row(*map(str, row))
if DIFF_CHANGED_KEY in self.__mediaDifferences[MediaDescriptor.TAGS_KEY].keys():
for changedTagKey in self.__mediaDifferences[MediaDescriptor.TAGS_KEY][DIFF_CHANGED_KEY]:
row = (f"changed media tag: key='{changedTagKey}' value='{currentTags[changedTagKey]}'->'{targetTags[changedTagKey]}'",)
self.differencesTable.add_row(*map(str, row))
if MediaDescriptor.TRACKS_KEY in self.__mediaDifferences.keys():
currentTracks = self.__currentMediaDescriptor.getAllTrackDescriptors() # 0,1,2,3
targetTracks = self.__targetMediaDescriptor.getAllTrackDescriptors() # 0 <- from DB
if DIFF_ADDED_KEY in self.__mediaDifferences[MediaDescriptor.TRACKS_KEY].keys():
#raise click.ClickException(f"add track {self.__mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_ADDED_KEY]}")
for addedTrackIndex in self.__mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_ADDED_KEY]:
addedTrack : Track = targetTracks[addedTrackIndex]
row = (f"added {addedTrack.getType().label()} track: index={addedTrackIndex} lang={addedTrack.getLanguage().threeLetter()}",)
self.differencesTable.add_row(*map(str, row))
if DIFF_REMOVED_KEY in self.__mediaDifferences[MediaDescriptor.TRACKS_KEY].keys():
for removedTrackIndex in self.__mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_REMOVED_KEY]:
row = (f"removed track: index={removedTrackIndex}",)
self.differencesTable.add_row(*map(str, row))
if DIFF_CHANGED_KEY in self.__mediaDifferences[MediaDescriptor.TRACKS_KEY].keys():
for changedTrackIndex in self.__mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_CHANGED_KEY].keys():
changedTrack : Track = targetTracks[changedTrackIndex]
changedTrackDiff : dict = self.__mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_CHANGED_KEY][changedTrackIndex]
if MediaDescriptor.TAGS_KEY in changedTrackDiff.keys():
if DIFF_ADDED_KEY in changedTrackDiff[MediaDescriptor.TAGS_KEY]:
for addedTagKey in changedTrackDiff[MediaDescriptor.TAGS_KEY][DIFF_ADDED_KEY]:
addedTagValue = changedTrack.getTags()[addedTagKey]
row = (f"changed {changedTrack.getType().label()} track index={changedTrackIndex} added key={addedTagKey} value={addedTagValue}",)
self.differencesTable.add_row(*map(str, row))
if DIFF_REMOVED_KEY in changedTrackDiff[MediaDescriptor.TAGS_KEY]:
for removedTagKey in changedTrackDiff[MediaDescriptor.TAGS_KEY][DIFF_REMOVED_KEY]:
row = (f"changed {changedTrack.getType().label()} track index={changedTrackIndex} removed key={removedTagKey}",)
self.differencesTable.add_row(*map(str, row))
if TrackDescriptor.DISPOSITION_SET_KEY in changedTrackDiff.keys():
if DIFF_ADDED_KEY in changedTrackDiff[TrackDescriptor.DISPOSITION_SET_KEY]:
for addedDisposition in changedTrackDiff[TrackDescriptor.DISPOSITION_SET_KEY][DIFF_ADDED_KEY]:
row = (f"changed {changedTrack.getType().label()} track index={changedTrackIndex} added disposition={addedDisposition.label()}",)
self.differencesTable.add_row(*map(str, row))
if DIFF_REMOVED_KEY in changedTrackDiff[TrackDescriptor.DISPOSITION_SET_KEY]:
for removedDisposition in changedTrackDiff[TrackDescriptor.DISPOSITION_SET_KEY][DIFF_REMOVED_KEY]:
row = (f"changed {changedTrack.getType().label()} track index={changedTrackIndex} removed disposition={removedDisposition.label()}",)
self.differencesTable.add_row(*map(str, row))
def on_mount(self):
if self.__currentPattern is None:
row = (' ', '<New show>', ' ') # Convert each element to a string before adding
self.showsTable.add_row(*map(str, row))
for show in self.__sc.getAllShows():
row = (int(show.id), show.name, show.year) # Convert each element to a string before adding
self.showsTable.add_row(*map(str, row))
for mediaTagKey, mediaTagValue in self.__currentMediaDescriptor.getTags().items():
row = (mediaTagKey, mediaTagValue) # Convert each element to a string before adding
self.mediaTagsTable.add_row(*map(str, row))
self.updateTracks()
if self.__currentPattern is not None:
showIdentifier = self.__currentPattern.getShowId()
showRowIndex = self.getRowIndexFromShowId(showIdentifier)
if showRowIndex is not None:
self.showsTable.move_cursor(row=showRowIndex)
self.query_one("#pattern_input", Input).value = self.__currentPattern.getPattern()
self.updateDifferences()
else:
self.query_one("#pattern_input", Input).value = self.__mediaFilename
self.highlightPattern(True)
def highlightPattern(self, state : bool):
if state:
self.query_one("#pattern_input", Input).styles.background = 'red'
else:
self.query_one("#pattern_input", Input).styles.background = None
def updateTracks(self):
self.tracksTable.clear()
trackDescriptorList = self.__currentMediaDescriptor.getAllTrackDescriptors()
typeCounter = {}
for td in trackDescriptorList:
trackType = td.getType()
if not trackType in typeCounter.keys():
typeCounter[trackType] = 0
dispoSet = td.getDispositionSet()
row = (td.getIndex(),
trackType.label(),
typeCounter[trackType],
td.getAudioLayout().label() if trackType == TrackType.AUDIO else ' ',
td.getLanguage().label(),
td.getTitle(),
'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
self.tracksTable.add_row(*map(str, row))
typeCounter[trackType] += 1
def compose(self):
# Create the DataTable widget
self.showsTable = DataTable(classes="two")
# Define the columns with headers
self.column_key_show_id = self.showsTable.add_column("ID", width=10)
self.column_key_show_name = self.showsTable.add_column("Name", width=50)
self.column_key_show_year = self.showsTable.add_column("Year", width=10)
self.showsTable.cursor_type = 'row'
self.mediaTagsTable = DataTable(classes="two")
# Define the columns with headers
self.column_key_track_tag_key = self.mediaTagsTable.add_column("Key", width=20)
self.column_key_track_tag_value = self.mediaTagsTable.add_column("Value", width=100)
self.mediaTagsTable.cursor_type = 'row'
self.tracksTable = DataTable(classes="two")
# Define the columns with headers
self.column_key_track_index = self.tracksTable.add_column("Index", width=5)
self.column_key_track_type = self.tracksTable.add_column("Type", width=10)
self.column_key_track_sub_index = self.tracksTable.add_column("Subindex", width=5)
self.column_key_track_layout = self.tracksTable.add_column("Layout", width=10)
self.column_key_track_language = self.tracksTable.add_column("Language", width=15)
self.column_key_track_title = self.tracksTable.add_column("Title", width=48)
self.column_key_track_default = self.tracksTable.add_column("Default", width=8)
self.column_key_track_forced = self.tracksTable.add_column("Forced", width=8)
self.tracksTable.cursor_type = 'row'
# Create the DataTable widget
self.differencesTable = DataTable(id='differences-table') # classes="triple"
# Define the columns with headers
self.column_key_differences = self.differencesTable.add_column("Differences (file->db)", width=70)
self.differencesTable.cursor_type = 'row'
yield Header()
with Grid():
# 1
yield Static("Show")
yield self.showsTable
yield Static(" ")
yield self.differencesTable
# 2
yield Static(" ")
yield Button("Substitute", id="pattern_button")
yield Static(" ", classes="two")
# 3
yield Static("Pattern")
yield Input(type="text", id='pattern_input', classes="two")
yield Static(" ")
# 4
yield Static(" ", classes="four")
# 5
yield Static("Media Tags")
yield self.mediaTagsTable
yield Static(" ")
# 6
yield Static(" ", classes="four")
# 7
yield Static(" ")
yield Button("Set Default", id="select_default_button")
yield Button("Set Forced", id="select_forced_button")
yield Static(" ")
# 8
yield Static("Streams")
yield self.tracksTable
yield Static(" ")
yield Footer()
def getPatternDescriptorFromInput(self):
"""Returns show id and pattern from corresponding inputs"""
patternDescriptor = {}
try:
patternDescriptor['show_id'] = self.getSelectedShowDescriptor().getId()
patternDescriptor['pattern'] = str(self.query_one("#pattern_input", Input).value)
except:
pass
return patternDescriptor
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "pattern_button":
INDICATOR_PATTERN = '([sS][0-9]+[eE][0-9]+)'
pattern = self.query_one("#pattern_input", Input).value
patternMatch = re.search(INDICATOR_PATTERN, pattern)
if patternMatch:
self.query_one("#pattern_input", Input).value = pattern.replace(patternMatch.group(1), INDICATOR_PATTERN)
if event.button.id == "select_default_button":
selectedTrackDescriptor = self.getSelectedTrackDescriptor()
self.__currentMediaDescriptor.setDefaultSubTrack(selectedTrackDescriptor.getType(), selectedTrackDescriptor.getSubIndex())
self.updateTracks()
if event.button.id == "select_forced_button":
selectedTrackDescriptor = self.getSelectedTrackDescriptor()
self.__currentMediaDescriptor.setForcedSubTrack(selectedTrackDescriptor.getType(), selectedTrackDescriptor.getSubIndex())
self.updateTracks()
def getSelectedTrackDescriptor(self):
"""Returns a partial track descriptor"""
try:
# Fetch the currently selected row when 'Enter' is pressed
#selected_row_index = self.table.cursor_row
row_key, col_key = self.tracksTable.coordinate_to_cell_key(self.tracksTable.cursor_coordinate)
if row_key is not None:
selected_track_data = self.tracksTable.get_row(row_key)
kwargs = {}
kwargs[TrackDescriptor.INDEX_KEY] = int(selected_track_data[0])
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = TrackType.fromLabel(selected_track_data[1])
kwargs[TrackDescriptor.SUB_INDEX_KEY] = int(selected_track_data[2])
return TrackDescriptor(**kwargs)
else:
return None
except CellDoesNotExist:
return None
def getSelectedShowDescriptor(self):
try:
# Fetch the currently selected row when 'Enter' is pressed
#selected_row_index = self.table.cursor_row
row_key, col_key = self.showsTable.coordinate_to_cell_key(self.showsTable.cursor_coordinate)
if row_key is not None:
selected_row_data = self.showsTable.get_row(row_key)
try:
kwargs = {}
kwargs[ShowDescriptor.ID_KEY] = int(selected_row_data[0])
kwargs[ShowDescriptor.NAME_KEY] = str(selected_row_data[1])
kwargs[ShowDescriptor.YEAR_KEY] = int(selected_row_data[2])
return ShowDescriptor(**kwargs)
except ValueError:
return None
except CellDoesNotExist:
return None
def handle_new_pattern(self, showDescriptor: ShowDescriptor):
show = (showDescriptor.getId(), showDescriptor.getName(), showDescriptor.getYear())
self.showsTable.add_row(*map(str, show))
showRowIndex = self.getRowIndexFromShowId(showDescriptor.getId())
if showRowIndex is not None:
self.showsTable.move_cursor(row=showRowIndex)
patternDescriptor = self.getPatternDescriptorFromInput()
if patternDescriptor:
patternId = self.__pc.addPattern(patternDescriptor)
self.highlightPattern(False)
for trackDescriptor in self.__currentMediaDescriptor.getAllTrackDescriptors():
self.__tc.addTrack(trackDescriptor, patternId = patternId)
def action_new_pattern(self):
if not self.__currentMediaDescriptor.checkDefaultAndForcedDispositions():
return
selectedShowDescriptor = self.getSelectedShowDescriptor()
#HINT: Callback is invoked after this method has exited. As a workaround the callback is executed directly
# from here with a mock-up screen result containing the necessary part of keys to perform correctly.
if selectedShowDescriptor is None:
self.app.push_screen(ShowDetailsScreen(), self.handle_new_pattern)
else:
self.handle_new_pattern(selectedShowDescriptor)
def action_update_pattern(self):
"""When updating the database the actions must reverse the difference (eq to diff db->file)"""
if self.__currentPattern is not None:
patternDescriptor = self.getPatternDescriptorFromInput()
if (patternDescriptor
and self.__currentPattern.getPattern() != patternDescriptor['pattern']):
return self.__pc.updatePattern(self.__currentPattern.getId(), patternDescriptor)
self.loadProperties()
if MediaDescriptor.TAGS_KEY in self.__mediaDifferences.keys():
if DIFF_ADDED_KEY in self.__mediaDifferences[MediaDescriptor.TAGS_KEY].keys():
for addedTagKey in self.__mediaDifferences[MediaDescriptor.TAGS_KEY][DIFF_ADDED_KEY]:
self.__tac.deleteMediaTagByKey(self.__currentPattern.getId(), addedTagKey)
if DIFF_REMOVED_KEY in self.__mediaDifferences[MediaDescriptor.TAGS_KEY].keys():
for removedTagKey in self.__mediaDifferences[MediaDescriptor.TAGS_KEY][DIFF_REMOVED_KEY]:
currentTags = self.__currentMediaDescriptor.getTags()
self.__tac.updateMediaTag(self.__currentPattern.getId(), removedTagKey, currentTags[removedTagKey])
if DIFF_CHANGED_KEY in self.__mediaDifferences[MediaDescriptor.TAGS_KEY].keys():
for changedTagKey in self.__mediaDifferences[MediaDescriptor.TAGS_KEY][DIFF_CHANGED_KEY]:
currentTags = self.__currentMediaDescriptor.getTags()
self.__tac.updateMediaTag(self.__currentPattern.getId(), changedTagKey, currentTags[changedTagKey])
if MediaDescriptor.TRACKS_KEY in self.__mediaDifferences.keys():
if DIFF_ADDED_KEY in self.__mediaDifferences[MediaDescriptor.TRACKS_KEY].keys():
for addedTrackIndex in self.__mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_ADDED_KEY]:
targetTracks = [t for t in self.__targetMediaDescriptor.getAllTrackDescriptors() if t.getIndex() == addedTrackIndex]
if targetTracks:
self.__tc.deleteTrack(targetTracks[0].getId()) # id
if DIFF_REMOVED_KEY in self.__mediaDifferences[MediaDescriptor.TRACKS_KEY].keys():
for removedTrackIndex, removedTrack in self.__mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_REMOVED_KEY].items():
# Track per inspect/update hinzufügen
self.__tc.addTrack(removedTrack, patternId = self.__currentPattern.getId())
if DIFF_CHANGED_KEY in self.__mediaDifferences[MediaDescriptor.TRACKS_KEY].keys():
# [vsTracks[tp].getIndex()] = trackDiff
for changedTrackIndex, changedTrackDiff in self.__mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_CHANGED_KEY].items():
changedTargetTracks = [t for t in self.__targetMediaDescriptor.getAllTrackDescriptors() if t.getIndex() == changedTrackIndex]
changedTargeTrackId = changedTargetTracks[0].getId() if changedTargetTracks else None
changedTargetTrackIndex = changedTargetTracks[0].getIndex() if changedTargetTracks else None
changedCurrentTracks = [t for t in self.__currentMediaDescriptor.getAllTrackDescriptors() if t.getIndex() == changedTrackIndex]
# changedCurrentTrackId #HINT: Undefined as track descriptors do not come from file with track_id
if TrackDescriptor.TAGS_KEY in changedTrackDiff.keys():
changedTrackTagsDiff = changedTrackDiff[TrackDescriptor.TAGS_KEY]
if DIFF_ADDED_KEY in changedTrackTagsDiff.keys():
for addedTrackTagKey in changedTrackTagsDiff[DIFF_ADDED_KEY]:
if changedTargetTracks:
self.__tac.deleteTrackTagByKey(changedTargeTrackId, addedTrackTagKey)
if DIFF_REMOVED_KEY in changedTrackTagsDiff.keys():
for removedTrackTagKey in changedTrackTagsDiff[DIFF_REMOVED_KEY]:
if changedCurrentTracks:
self.__tac.updateTrackTag(changedTargeTrackId, removedTrackTagKey, changedCurrentTracks[0].getTags()[removedTrackTagKey])
if DIFF_CHANGED_KEY in changedTrackTagsDiff.keys():
for changedTrackTagKey in changedTrackTagsDiff[DIFF_CHANGED_KEY]:
if changedCurrentTracks:
self.__tac.updateTrackTag(changedTargeTrackId, changedTrackTagKey, changedCurrentTracks[0].getTags()[changedTrackTagKey])
if TrackDescriptor.DISPOSITION_SET_KEY in changedTrackDiff.keys():
changedTrackDispositionDiff = changedTrackDiff[TrackDescriptor.DISPOSITION_SET_KEY]
if DIFF_ADDED_KEY in changedTrackDispositionDiff.keys():
for changedTrackAddedDisposition in changedTrackDispositionDiff[DIFF_ADDED_KEY]:
if changedTargetTrackIndex is not None:
self.__tc.setDispositionState(self.__currentPattern.getId(), changedTargetTrackIndex, changedTrackAddedDisposition, False)
if DIFF_REMOVED_KEY in changedTrackDispositionDiff.keys():
for changedTrackRemovedDisposition in changedTrackDispositionDiff[DIFF_REMOVED_KEY]:
if changedTargetTrackIndex is not None:
self.__tc.setDispositionState(self.__currentPattern.getId(), changedTargetTrackIndex, changedTrackRemovedDisposition, True)
self.updateDifferences()
def action_edit_pattern(self):
patternDescriptor = {}
patternDescriptor['show_id'] = self.getSelectedShowDescriptor().getId()
patternDescriptor['pattern'] = self.getPatternFromInput()
if patternDescriptor['pattern']:
selectedPatternId = self.__pc.findPattern(patternDescriptor)
if selectedPatternId is None:
raise click.ClickException(f"MediaDetailsScreen.action_edit_pattern(): Pattern to edit has no id")
self.app.push_screen(PatternDetailsScreen(patternId = selectedPatternId, showId = self.getSelectedShowDescriptor().getId()), self.handle_edit_pattern) # <-
def handle_edit_pattern(self, screenResult):
self.query_one("#pattern_input", Input).value = screenResult['pattern']
self.updateDifferences()

View File

@@ -1,10 +1,13 @@
import click
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship, sessionmaker, Mapped, backref from sqlalchemy.orm import relationship, sessionmaker, Mapped, backref
from .show import Base from .show import Base, Show
from .track import Track from .track import Track
from ffx.media_descriptor import MediaDescriptor from ffx.media_descriptor import MediaDescriptor
from ffx.show_descriptor import ShowDescriptor
class Pattern(Base): class Pattern(Base):
@@ -20,57 +23,46 @@ class Pattern(Base):
# v1.x # v1.x
show_id = Column(Integer, ForeignKey('shows.id', ondelete="CASCADE")) show_id = Column(Integer, ForeignKey('shows.id', ondelete="CASCADE"))
show = relationship('Show', back_populates='patterns') show = relationship(Show, back_populates='patterns', lazy='joined')
# v2.0 # v2.0
# show_id: Mapped[int] = mapped_column(ForeignKey("shows.id", ondelete="CASCADE")) # show_id: Mapped[int] = mapped_column(ForeignKey("shows.id", ondelete="CASCADE"))
# 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", lazy='joined')
media_tags = relationship('MediaTag', back_populates='pattern', cascade="all, delete") media_tags = relationship('MediaTag', back_populates='pattern', cascade="all, delete", lazy='joined')
# def getDescriptor(self): def getId(self):
# return int(self.id)
# 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 getShowId(self):
return int(self.show_id)
def getShow(self): def getShowDescriptor(self) -> ShowDescriptor:
pass click.echo(f"self.show {self.show} id={self.show_id}")
return self.show.getDescriptor()
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): def getId(self):
return int(self.id) return int(self.id)
def getPattern(self): def getPattern(self):
return str(self.pattern) return str(self.pattern)
def getShowId(self):
return int(self.show_id)
def getTags(self): def getTags(self):
return {str(k.value):str(v.value) for (k,v) in self.media_tags} return {str(t.key):str(t.value) for t in self.media_tags}
def getMediaDescriptor(self):
kwargs = {}
kwargs[MediaDescriptor.TAGS_KEY] = self.getTags()
kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY] = []
for track in self.tracks:
kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY].append(track.getDescriptor())
return MediaDescriptor(**kwargs)

View File

@@ -2,6 +2,9 @@
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship, declarative_base, sessionmaker from sqlalchemy.orm import relationship, declarative_base, sessionmaker
from ffx.show_descriptor import ShowDescriptor
Base = declarative_base() Base = declarative_base()
class Show(Base): class Show(Base):
@@ -35,23 +38,22 @@ class Show(Base):
# v2.0 # v2.0
# patterns: Mapped[List["Pattern"]] = relationship(back_populates="show", cascade="all, delete") # patterns: Mapped[List["Pattern"]] = relationship(back_populates="show", cascade="all, delete")
index_season_digits = Column(Integer, default=2) index_season_digits = Column(Integer, default=ShowDescriptor.DEFAULT_INDEX_SEASON_DIGITS)
index_episode_digits = Column(Integer, default=2) index_episode_digits = Column(Integer, default=ShowDescriptor.DEFAULT_INDEX_EPISODE_DIGITS)
indicator_season_digits = Column(Integer, default=2) indicator_season_digits = Column(Integer, default=ShowDescriptor.DEFAULT_INDICATOR_SEASON_DIGITS)
indicator_episode_digits = Column(Integer, default=2) indicator_episode_digits = Column(Integer, default=ShowDescriptor.DEFAULT_INDICATOR_EPISODE_DIGITS)
def getDesciptor(self): def getDescriptor(self):
descriptor = {} kwargs = {}
descriptor['id'] = int(self.id) kwargs[ShowDescriptor.ID_KEY] = int(self.id)
descriptor['name'] = str(self.name) kwargs[ShowDescriptor.NAME_KEY] = str(self.name)
descriptor['year'] = int(self.year) kwargs[ShowDescriptor.YEAR_KEY] = int(self.year)
kwargs[ShowDescriptor.INDEX_SEASON_DIGITS_KEY] = int(self.index_season_digits)
descriptor['index_season_digits'] = int(self.index_season_digits) kwargs[ShowDescriptor.INDEX_EPISODE_DIGITS_KEY] = int(self.index_episode_digits)
descriptor['index_episode_digits'] = int(self.index_episode_digits) kwargs[ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY] = int(self.indicator_season_digits)
descriptor['indicator_season_digits'] = int(self.indicator_season_digits) kwargs[ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY] = int(self.indicator_episode_digits)
descriptor['indicator_episode_digits'] = int(self.indicator_episode_digits)
return ShowDescriptor(**kwargs)
return descriptor

View File

@@ -11,6 +11,8 @@ from ffx.iso_language import IsoLanguage
from ffx.track_disposition import TrackDisposition from ffx.track_disposition import TrackDisposition
from ffx.track_descriptor import TrackDescriptor from ffx.track_descriptor import TrackDescriptor
from ffx.audio_layout import AudioLayout
import click import click
class Track(Base): class Track(Base):
@@ -32,21 +34,17 @@ class Track(Base):
track_type = Column(Integer) # TrackType track_type = Column(Integer) # TrackType
index = Column(Integer) index = Column(Integer)
sub_index = Column(Integer) source_index = Column(Integer)
# v1.x # v1.x
pattern_id = Column(Integer, ForeignKey('patterns.id', ondelete="CASCADE")) pattern_id = Column(Integer, ForeignKey('patterns.id', ondelete="CASCADE"))
pattern = relationship('Pattern', back_populates='tracks') pattern = relationship('Pattern', back_populates='tracks')
# language = Column(String) # IsoLanguage threeLetter
# title = Column(String)
track_tags = relationship('TrackTag', back_populates='track', cascade="all, delete", lazy="joined") track_tags = relationship('TrackTag', back_populates='track', cascade="all, delete", lazy="joined")
disposition_flags = Column(Integer) disposition_flags = Column(Integer)
audio_layout = Column(Integer)
def __init__(self, **kwargs): def __init__(self, **kwargs):
@@ -55,10 +53,6 @@ class Track(Base):
if trackType is not None: if trackType is not None:
self.track_type = int(trackType) 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()) dispositionSet = kwargs.pop(TrackDescriptor.DISPOSITION_SET_KEY, set())
self.disposition_flags = int(TrackDisposition.toFlags(dispositionSet)) self.disposition_flags = int(TrackDisposition.toFlags(dispositionSet))
@@ -66,7 +60,7 @@ class Track(Base):
@classmethod @classmethod
def fromStreamObj(cls, streamObj, subIndex, patternId): def fromFfprobeStreamObj(cls, streamObj, patternId):
"""{ """{
'index': 4, 'index': 4,
'codec_name': 'hdmv_pgs_subtitle', 'codec_name': 'hdmv_pgs_subtitle',
@@ -133,14 +127,15 @@ class Track(Base):
""" """
trackType = streamObj['codec_type'] trackType = streamObj[TrackDescriptor.FFPROBE_CODEC_TYPE_KEY]
if trackType in [t.label() for t in TrackType]: if trackType in [t.label() for t in TrackType]:
return cls(pattern_id = patternId, return cls(pattern_id = patternId,
sub_index = int(subIndex),
track_type = trackType, 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])) disposition_flags = sum([2**t.index() for (k,v) in streamObj[TrackDescriptor.FFPROBE_DISPOSITION_KEY].items()
if v and (t := TrackDisposition.find(k)) is not None]),
audio_layout = AudioLayout.identify(streamObj))
else: else:
return None return None
@@ -154,10 +149,13 @@ class Track(Base):
def getType(self): def getType(self):
return TrackType.fromIndex(self.track_type) return TrackType.fromIndex(self.track_type)
def getSubIndex(self): def getIndex(self):
return int(self.sub_index) return int(self.index) if self.index is not None else -1
def getSourceIndex(self):
return int(self.source_index) if self.source_index is not None else -1
def getLanguage(self): def getLanguage(self):
tags = {t.key:t.value for t in self.track_tags} tags = {t.key:t.value for t in self.track_tags}
return IsoLanguage.findThreeLetter(tags['language']) if 'language' in tags.keys() else IsoLanguage.UNDEFINED return IsoLanguage.findThreeLetter(tags['language']) if 'language' in tags.keys() else IsoLanguage.UNDEFINED
@@ -169,5 +167,40 @@ class Track(Base):
def getDispositionSet(self): def getDispositionSet(self):
return TrackDisposition.toSet(self.disposition_flags) return TrackDisposition.toSet(self.disposition_flags)
def getAudioLayout(self):
return AudioLayout.fromIndex(self.audio_layout)
def getTags(self): def getTags(self):
return {str(t.key):str(t.value) for t in self.track_tags} return {str(t.key):str(t.value) for t in self.track_tags}
def setDisposition(self, disposition : TrackDisposition):
self.disposition_flags = self.disposition_flags | int(2**disposition.index())
def resetDisposition(self, disposition : TrackDisposition):
self.disposition_flags = self.disposition_flags & sum([2**d.index() for d in TrackDisposition if d != disposition])
def getDisposition(self, disposition : TrackDisposition):
return bool(self.disposition_flags & 2**disposition.index())
def getDescriptor(self, subIndex : int = -1) -> TrackDescriptor:
kwargs = {}
kwargs[TrackDescriptor.ID_KEY] = self.getId()
kwargs[TrackDescriptor.PATTERN_ID_KEY] = self.getPatternId()
kwargs[TrackDescriptor.INDEX_KEY] = self.getIndex()
kwargs[TrackDescriptor.SOURCE_INDEX_KEY] = self.getSourceIndex()
if subIndex > -1:
kwargs[TrackDescriptor.SUB_INDEX_KEY] = subIndex
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = self.getType()
kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = self.getDispositionSet()
kwargs[TrackDescriptor.TAGS_KEY] = self.getTags()
kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = self.getAudioLayout()
return TrackDescriptor(**kwargs)

View File

@@ -2,8 +2,6 @@ import click, re
from ffx.model.pattern import Pattern from ffx.model.pattern import Pattern
from .media_descriptor import MediaDescriptor
class PatternController(): class PatternController():
@@ -18,16 +16,16 @@ class PatternController():
try: try:
s = self.Session() s = self.Session()
q = s.query(Pattern).filter(Pattern.show_id == int(patternDescriptor['show_id']), Pattern.pattern == str(patternDescriptor['pattern'])) q = s.query(Pattern).filter(Pattern.show_id == int(patternDescriptor['show_id']))
if not q.count(): if not q.count():
pattern = Pattern(show_id = int(patternDescriptor['show_id']), pattern = Pattern(show_id = int(patternDescriptor['show_id']),
pattern = str(patternDescriptor['pattern'])) pattern = str(patternDescriptor['pattern']))
s.add(pattern) s.add(pattern)
s.commit() s.commit()
return patternDescriptor return pattern.getId()
else: else:
return {} return None
except Exception as ex: except Exception as ex:
raise click.ClickException(f"PatternController.addPattern(): {repr(ex)}") raise click.ClickException(f"PatternController.addPattern(): {repr(ex)}")
@@ -88,12 +86,7 @@ class PatternController():
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(): return q.first() if q.count() else None
# pattern = q.first()
#return self.getPatternDict(pattern)
return q.first()
else:
return None
except Exception as ex: except Exception as ex:
raise click.ClickException(f"PatternController.getPattern(): {repr(ex)}") raise click.ClickException(f"PatternController.getPattern(): {repr(ex)}")
@@ -123,52 +116,27 @@ class PatternController():
s.close() s.close()
def matchFilename(self, filename): def matchFilename(self, filename : str) -> re.Match:
#SEASON_PATTERN = '[sS]([0-9]+)'
#EPISODE_PATTERN = '[eE]([0-9]+)'
#result = {}
try: try:
s = self.Session() s = self.Session()
q = s.query(Pattern) q = s.query(Pattern)
matchedPatterns = [p for p in q.all() if re.search(p.pattern, filename)] matchResult = {}
if matchedPatterns: for pattern in q.all():
return matchedPatterns[0] patternMatch = re.search(str(pattern.pattern), str(filename))
else: if patternMatch:
return None matchResult['match'] = patternMatch
matchResult['pattern'] = pattern
# 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))
return matchResult
except Exception as ex: except Exception as ex:
raise click.ClickException(f"PatternController.findPattern(): {repr(ex)}") raise click.ClickException(f"PatternController.matchFilename(): {repr(ex)}")
finally: finally:
s.close() s.close()
return result
def getMediaDescriptor(self, patternId): def getMediaDescriptor(self, patternId):
try: try:
@@ -176,9 +144,9 @@ class PatternController():
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() return q.first().getMediaDescriptor()
#return self.getPatternDict(pattern) else:
return pattern.getMediaDescriptor() return None
except Exception as ex: except Exception as ex:
raise click.ClickException(f"PatternController.getPatternDescriptor(): {repr(ex)}") raise click.ClickException(f"PatternController.getPatternDescriptor(): {repr(ex)}")

View File

@@ -1,12 +1,8 @@
import click import click
from textual import events
from textual.app import App, ComposeResult
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import Header, Footer, Placeholder, Label, ListView, ListItem, Static, DataTable, Button, Input from textual.widgets import Header, Footer, Static, Button
from textual.containers import Grid, Horizontal from textual.containers import Grid
from ffx.model.pattern import Pattern
from .show_controller import ShowController from .show_controller import ShowController
from .pattern_controller import PatternController from .pattern_controller import PatternController
@@ -57,7 +53,7 @@ class PatternDeleteScreen(Screen):
self.pattern_id = patternId self.pattern_id = patternId
self.pattern_obj = self.__pc.getPatternDescriptor(patternId) if patternId is not None else {} self.pattern_obj = self.__pc.getPatternDescriptor(patternId) if patternId is not None else {}
self.show_obj = self.__sc.getShowDesciptor(showId) if showId is not None else {} self.show_obj = self.__sc.getShowDescriptor(showId) if showId is not None else {}
def on_mount(self): def on_mount(self):

View File

@@ -3,19 +3,24 @@ import click, re
from textual import events from textual import events
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import Header, Footer, Placeholder, Label, ListView, ListItem, Static, DataTable, Button, Input from textual.widgets import Header, Footer, Static, Button, Input, DataTable
from textual.containers import Grid, Horizontal from textual.containers import Grid
from ffx.model.show import Show from ffx.model.show import Show
from ffx.model.pattern import Pattern from ffx.model.pattern import Pattern
from ffx.model.track import Track
from .pattern_controller import PatternController from .pattern_controller import PatternController
from .show_controller import ShowController from .show_controller import ShowController
from .track_controller import TrackController from .track_controller import TrackController
from .tag_controller import TagController
from .track_details_screen import TrackDetailsScreen from .track_details_screen import TrackDetailsScreen
from .track_delete_screen import TrackDeleteScreen from .track_delete_screen import TrackDeleteScreen
from .tag_details_screen import TagDetailsScreen
from .tag_delete_screen import TagDeleteScreen
from ffx.track_type import TrackType from ffx.track_type import TrackType
from ffx.track_disposition import TrackDisposition from ffx.track_disposition import TrackDisposition
@@ -30,8 +35,8 @@ class PatternDetailsScreen(Screen):
CSS = """ CSS = """
Grid { Grid {
grid-size: 5 12; grid-size: 5 13;
grid-rows: 2 2 2 2 2 6 2 2 6 2 2 2; grid-rows: 2 2 2 2 2 8 2 2 8 2 2 2 2;
grid-columns: 25 25 25 25 25; grid-columns: 25 25 25 25 25;
height: 100%; height: 100%;
width: 100%; width: 100%;
@@ -68,6 +73,10 @@ class PatternDetailsScreen(Screen):
height: 100%; height: 100%;
border: solid green; border: solid green;
} }
.yellow {
tint: yellow 40%;
}
""" """
def __init__(self, patternId = None, showId = None): def __init__(self, patternId = None, showId = None):
@@ -79,12 +88,13 @@ class PatternDetailsScreen(Screen):
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.__tac = TagController(context = self.context)
self.__pattern = self.__pc.getPattern(patternId) if patternId is not None else None self.__pattern : Pattern = self.__pc.getPattern(patternId) if patternId is not None else None
self.__showDescriptor = self.__sc.getShowDescriptor(showId) if showId is not None else None
self.show_obj = self.__sc.getShowDesciptor(showId) if showId is not None else {}
#TODO: per controller
def loadTracks(self, show_id): def loadTracks(self, show_id):
try: try:
@@ -104,88 +114,92 @@ class PatternDetailsScreen(Screen):
s.close() s.close()
def updateAudioTracks(self): def updateTracks(self):
self.audioStreamsTable.clear() self.tracksTable.clear()
if self.__pattern is not None: if self.__pattern is not None:
audioTracks = self.__tc.findAudioTracks(self.__pattern.getId()) tracks = self.__tc.findTracks(self.__pattern.getId())
for at in audioTracks: typeCounter = {}
dispoSet = at.getDispositionSet() for tr in tracks:
row = (at.getSubIndex(), td : TrackDescriptor = tr.getDescriptor()
" ",
at.getLanguage().label(), trackType = td.getType()
at.getTitle(), if not trackType in typeCounter.keys():
typeCounter[trackType] = 0
dispoSet = td.getDispositionSet()
row = (td.getIndex(),
trackType.label(),
typeCounter[trackType],
td.getAudioLayout().label() if trackType == TrackType.AUDIO else ' ',
td.getLanguage().label(),
td.getTitle(),
'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No', 'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
'Yes' if TrackDisposition.FORCED in dispoSet else 'No') 'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
self.audioStreamsTable.add_row(*map(str, row)) self.tracksTable.add_row(*map(str, row))
def updateSubtitleTracks(self): typeCounter[trackType] += 1
self.subtitleStreamsTable.clear() def updateTags(self):
self.tagsTable.clear()
if self.__pattern is not None: if self.__pattern is not None:
subtitleTracks = self.__tc.findSubtitleTracks(self.__pattern.getId()) # raise click.ClickException(f"patternid={self.__pattern.getId()}") # 1
for st in subtitleTracks: tags = self.__tac.findAllMediaTags(self.__pattern.getId())
dispoSet = st.getDispositionSet() #raise click.ClickException(f"tags={tags}") # encoder:blah
row = (st.getSubIndex(), for tagKey, tagValue in tags.items():
" ", row = (tagKey, tagValue)
st.getLanguage().label(), self.tagsTable.add_row(*map(str, row))
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.show_obj: if not self.__showDescriptor is None:
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.__showDescriptor.getId()} - {self.__showDescriptor.getName()} ({self.__showDescriptor.getYear()})")
if self.__pattern is not None: if self.__pattern is not None:
self.query_one("#pattern_input", Input).value = str(self.__pattern.getPattern()) self.query_one("#pattern_input", Input).value = str(self.__pattern.getPattern())
self.updateAudioTracks() self.updateTags()
self.updateSubtitleTracks() self.updateTracks()
def compose(self): def compose(self):
self.audioStreamsTable = DataTable(classes="five")
self.tagsTable = DataTable(classes="five")
# Define the columns with headers # Define the columns with headers
self.column_key_audio_subid = self.audioStreamsTable.add_column("Subindex", width=20) self.column_key_tag_key = self.tagsTable.add_column("Key", width=10)
self.column_key_audio_layout = self.audioStreamsTable.add_column("Layout", width=20) self.column_key_tag_value = self.tagsTable.add_column("Value", width=100)
self.column_key_audio_language = self.audioStreamsTable.add_column("Language", width=20)
self.column_key_audio_title = self.audioStreamsTable.add_column("Title", width=30)
self.column_key_audio_default = self.audioStreamsTable.add_column("Default", width=10)
self.column_key_audio_forced = self.audioStreamsTable.add_column("Forced", width=10)
self.audioStreamsTable.cursor_type = 'row' self.tagsTable.cursor_type = 'row'
self.subtitleStreamsTable = DataTable(classes="five") self.tracksTable = DataTable(id="tracks_table", classes="five")
# Define the columns with headers self.column_key_track_index = self.tracksTable.add_column("Index", width=5)
self.column_key_subtitle_subid = self.subtitleStreamsTable.add_column("Subindex", width=20) self.column_key_track_type = self.tracksTable.add_column("Type", width=10)
self.column_key_subtitle_spacer = self.subtitleStreamsTable.add_column(" ", width=20) self.column_key_track_sub_index = self.tracksTable.add_column("Subindex", width=5)
self.column_key_subtitle_language = self.subtitleStreamsTable.add_column("Language", width=20) self.column_key_track_audio_layout = self.tracksTable.add_column("Layout", width=10)
self.column_key_subtitle_title = self.subtitleStreamsTable.add_column("Title", width=30) self.column_key_track_language = self.tracksTable.add_column("Language", width=15)
self.column_key_subtitle_default = self.subtitleStreamsTable.add_column("Default", width=10) self.column_key_track_title = self.tracksTable.add_column("Title", width=48)
self.column_key_subtitle_forced = self.subtitleStreamsTable.add_column("Forced", width=10) self.column_key_track_default = self.tracksTable.add_column("Default", width=8)
self.column_key_track_forced = self.tracksTable.add_column("Forced", width=8)
self.subtitleStreamsTable.cursor_type = 'row' self.tracksTable.cursor_type = 'row'
yield Header() yield Header()
@@ -199,7 +213,7 @@ class PatternDetailsScreen(Screen):
# 2 # 2
yield Static("from show") yield Static("from show")
yield Static("", id="showlabel", classes="three") yield Static("", id="showlabel", classes="three")
yield Button("Substitute pattern", id="patternbutton") yield Button("Substitute pattern", id="pattern_button")
# 3 # 3
yield Static(" ", classes="five") yield Static(" ", classes="five")
@@ -207,44 +221,51 @@ class PatternDetailsScreen(Screen):
yield Static(" ", classes="five") yield Static(" ", classes="five")
# 5 # 5
yield Static("Audio streams") yield Static("Media Tags")
yield Static(" ") yield Static(" ")
if self.__pattern is not None: if self.__pattern is not None:
yield Button("Add", id="button_add_audio_stream") yield Button("Add", id="button_add_tag")
yield Button("Edit", id="button_edit_audio_stream") yield Button("Edit", id="button_edit_tag")
yield Button("Delete", id="button_delete_audio_stream") yield Button("Delete", id="button_delete_tag")
else: else:
yield Static("") yield Static(" ")
yield Static("") yield Static(" ")
yield Static("") yield Static(" ")
# 6 # 6
yield self.audioStreamsTable yield self.tagsTable
# 7 # 7
yield Static(" ", classes="five") yield Static(" ", classes="five")
# 8 # 8
yield Static("Subtitle streams") yield Static("Streams")
yield Static(" ") yield Static(" ")
if self.__pattern is not None: if self.__pattern is not None:
yield Button("Add", id="button_add_subtitle_stream") yield Button("Add", id="button_add_track")
yield Button("Edit", id="button_edit_subtitle_stream") yield Button("Edit", id="button_edit_track")
yield Button("Delete", id="button_delete_subtitle_stream") yield Button("Delete", id="button_delete_track")
else: else:
yield Static("") yield Static(" ")
yield Static("") yield Static(" ")
yield Static("") yield Static(" ")
# 9 # 9
yield self.subtitleStreamsTable yield self.tracksTable
# 10 # 10
yield Static(" ", classes="five") yield Static(" ", classes="five")
# 11 # 11
yield Static(" ", classes="five")
# 12
yield Button("Save", id="save_button") yield Button("Save", id="save_button")
yield Button("Cancel", id="cancel_button") yield Button("Cancel", id="cancel_button")
yield Static(" ", classes="three")
# 13
yield Static(" ", classes="five")
yield Footer() yield Footer()
@@ -254,7 +275,7 @@ class PatternDetailsScreen(Screen):
def getSelectedAudioTrackDescriptor(self): def getSelectedTrackDescriptor(self):
if not self.__pattern: if not self.__pattern:
return None return None
@@ -263,25 +284,15 @@ class PatternDetailsScreen(Screen):
# Fetch the currently selected row when 'Enter' is pressed # Fetch the currently selected row when 'Enter' is pressed
#selected_row_index = self.table.cursor_row #selected_row_index = self.table.cursor_row
row_key, col_key = self.audioStreamsTable.coordinate_to_cell_key(self.audioStreamsTable.cursor_coordinate) row_key, col_key = self.tracksTable.coordinate_to_cell_key(self.tracksTable.cursor_coordinate)
if row_key is not None: if row_key is not None:
selected_track_data = self.audioStreamsTable.get_row(row_key) selected_track_data = self.tracksTable.get_row(row_key)
subIndex = int(selected_track_data[0]) trackIndex = int(selected_track_data[0])
trackSubIndex = int(selected_track_data[2])
audioTrack = self.__tc.findTrack(self.__pattern.getId(), TrackType.AUDIO, subIndex) return self.__tc.getTrack(self.__pattern.getId(), trackIndex).getDescriptor(subIndex=trackSubIndex)
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
@@ -290,34 +301,22 @@ class PatternDetailsScreen(Screen):
return None return None
def getSelectedSubtitleTrackDescriptor(self):
if not self.__pattern is None: def getSelectedTag(self):
return None
try: try:
# Fetch the currently selected row when 'Enter' is pressed # Fetch the currently selected row when 'Enter' is pressed
#selected_row_index = self.table.cursor_row #selected_row_index = self.table.cursor_row
row_key, col_key = self.subtitleStreamsTable.coordinate_to_cell_key(self.subtitleStreamsTable.cursor_coordinate) row_key, col_key = self.tagsTable.coordinate_to_cell_key(self.tagsTable.cursor_coordinate)
if row_key is not None: if row_key is not None:
selected_track_data = self.subtitleStreamsTable.get_row(row_key) selected_tag_data = self.tagsTable.get_row(row_key)
subIndex = int(selected_track_data[0]) tagKey = str(selected_tag_data[0])
tagValue = str(selected_tag_data[1])
subtitleTrack = self.__tc.findTrack(self.__pattern.getId(), TrackType.SUBTITLE, subIndex) return tagKey, tagValue
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
@@ -333,7 +332,7 @@ class PatternDetailsScreen(Screen):
if event.button.id == "save_button": if event.button.id == "save_button":
patternDescriptor = {} patternDescriptor = {}
patternDescriptor['show_id'] = self.show_obj['id'] patternDescriptor['show_id'] = self.__showDescriptor.getId()
patternDescriptor['pattern'] = self.getPatternFromInput() patternDescriptor['pattern'] = self.getPatternFromInput()
if self.__pattern is not None: if self.__pattern is not None:
@@ -345,14 +344,15 @@ class PatternDetailsScreen(Screen):
self.app.pop_screen() self.app.pop_screen()
else: else:
if self.__pc.addPattern(patternDescriptor): patternId = self.__pc.addPattern(patternDescriptor)
if patternId is not None:
self.dismiss(patternDescriptor) self.dismiss(patternDescriptor)
else: else:
#TODO: Meldung #TODO: Meldung
self.app.pop_screen() self.app.pop_screen()
if event.button.id == "cancel_button": if event.button.id == "cancel_button":
self.app.pop_screen() self.app.pop_screen()
@@ -360,28 +360,33 @@ class PatternDetailsScreen(Screen):
# Save pattern when just created before adding streams # Save pattern when just created before adding streams
if self.__pattern is not None: if self.__pattern is not None:
if event.button.id == "button_add_audio_stream": numTracks = len(self.tracksTable.rows)
self.app.push_screen(TrackDetailsScreen(trackType = TrackType.AUDIO, patternId = self.__pattern.getId(), subIndex = len(self.audioStreamsTable.rows)), self.handle_add_track)
selectedAudioTrack = self.getSelectedAudioTrackDescriptor() if event.button.id == "button_add_track":
if selectedAudioTrack is not None: self.app.push_screen(TrackDetailsScreen(patternId = self.__pattern.getId(), index = numTracks), self.handle_add_track)
if event.button.id == "button_edit_audio_stream":
self.app.push_screen(TrackDetailsScreen(trackDescriptor = selectedAudioTrack), self.handle_edit_track) selectedTrack = self.getSelectedTrackDescriptor()
if event.button.id == "button_delete_audio_stream": if selectedTrack is not None:
self.app.push_screen(TrackDeleteScreen(trackDescriptor = selectedAudioTrack), self.handle_delete_track) if event.button.id == "button_edit_track":
self.app.push_screen(TrackDetailsScreen(trackDescriptor = selectedTrack), self.handle_edit_track)
if event.button.id == "button_add_subtitle_stream": if event.button.id == "button_delete_track":
self.app.push_screen(TrackDetailsScreen(trackType = TrackType.SUBTITLE, patternId = self.__pattern.getId(), subIndex = len(self.subtitleStreamsTable.rows)), self.handle_add_track) self.app.push_screen(TrackDeleteScreen(trackDescriptor = selectedTrack), self.handle_delete_track)
selectedSubtitleTrack = self.getSelectedSubtitleTrackDescriptor()
if selectedSubtitleTrack is not None:
if event.button.id == "button_edit_subtitle_stream":
self.app.push_screen(TrackDetailsScreen(trackDescriptor = selectedSubtitleTrack), self.handle_edit_track)
if event.button.id == "button_delete_subtitle_stream":
self.app.push_screen(TrackDeleteScreen(trackDescriptor = selectedSubtitleTrack), self.handle_delete_track)
if event.button.id == "patternbutton": if event.button.id == "button_add_tag":
if self.__pattern is not None:
self.app.push_screen(TagDetailsScreen(), self.handle_update_tag)
if event.button.id == "button_edit_tag":
tagKey, tagValue = self.getSelectedTag()
self.app.push_screen(TagDetailsScreen(key=tagKey, value=tagValue), self.handle_update_tag)
if event.button.id == "button_delete_tag":
tagKey, tagValue = self.getSelectedTag()
self.app.push_screen(TagDeleteScreen(key=tagKey, value=tagValue), self.handle_delete_tag)
if event.button.id == "pattern_button":
INDICATOR_PATTERN = '([sS][0-9]+[eE][0-9]+)' INDICATOR_PATTERN = '([sS][0-9]+[eE][0-9]+)'
@@ -393,70 +398,60 @@ class PatternDetailsScreen(Screen):
self.query_one("#pattern_input", Input).value = pattern.replace(patternMatch.group(1), INDICATOR_PATTERN) self.query_one("#pattern_input", Input).value = pattern.replace(patternMatch.group(1), INDICATOR_PATTERN)
def handle_add_track(self, trackDescriptor): def handle_add_track(self, trackDescriptor : TrackDescriptor):
dispoSet = trackDescriptor.getDispositionSet() dispoSet = trackDescriptor.getDispositionSet()
trackType = trackDescriptor.getType() trackType = trackDescriptor.getType()
index = trackDescriptor.getIndex()
subIndex = trackDescriptor.getSubIndex() subIndex = trackDescriptor.getSubIndex()
language = trackDescriptor.getLanguage() language = trackDescriptor.getLanguage()
title = trackDescriptor.getTitle() title = trackDescriptor.getTitle()
if trackType == TrackType.AUDIO: row = (index,
trackType.label(),
subIndex,
" ",
language.label(),
title,
'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
row = (subIndex, self.tracksTable.add_row(*map(str, row))
" ",
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 trackType == TrackType.SUBTITLE:
row = (subIndex,
" ",
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 : TrackDescriptor): def handle_edit_track(self, trackDescriptor : TrackDescriptor):
try: try:
if trackDescriptor.getType() == TrackType.AUDIO:
row_key, col_key = self.audioStreamsTable.coordinate_to_cell_key(self.audioStreamsTable.cursor_coordinate) row_key, col_key = self.tracksTable.coordinate_to_cell_key(self.tracksTable.cursor_coordinate)
self.audioStreamsTable.update_cell(row_key, self.column_key_audio_language, trackDescriptor.getLanguage().label()) self.tracksTable.update_cell(row_key, self.column_key_track_audio_layout, trackDescriptor.getAudioLayout().label())
self.audioStreamsTable.update_cell(row_key, self.column_key_audio_title, trackDescriptor.getTitle()) self.tracksTable.update_cell(row_key, self.column_key_track_language, trackDescriptor.getLanguage().label())
self.audioStreamsTable.update_cell(row_key, self.column_key_audio_default, 'Yes' if TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() else 'No') self.tracksTable.update_cell(row_key, self.column_key_track_title, trackDescriptor.getTitle())
self.audioStreamsTable.update_cell(row_key, self.column_key_audio_forced, 'Yes' if TrackDisposition.FORCED in trackDescriptor.getDispositionSet() else 'No') self.tracksTable.update_cell(row_key, self.column_key_track_default, 'Yes' if TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() else 'No')
self.tracksTable.update_cell(row_key, self.column_key_track_forced, 'Yes' if TrackDisposition.FORCED in trackDescriptor.getDispositionSet() else 'No')
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.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: except CellDoesNotExist:
pass pass
def handle_delete_track(self, trackDescriptor : TrackDescriptor): def handle_delete_track(self, trackDescriptor : TrackDescriptor):
self.updateTracks()
try:
if trackDescriptor.getType() == TrackType.AUDIO:
self.updateAudioTracks()
if trackDescriptor.getType() == TrackType.SUBTITLE:
self.updateSubtitleTracks()
except CellDoesNotExist: def handle_update_tag(self, tag):
pass
if self.__pattern is None:
raise click.ClickException(f"PatternDetailsScreen.handle_update_tag: pattern not set")
if self.__tac.updateMediaTag(self.__pattern.getId(), tag[0], tag[1]) is not None:
self.updateTags()
def handle_delete_tag(self, tag):
if self.__pattern is None:
raise click.ClickException(f"PatternDetailsScreen.handle_delete_tag: pattern not set")
if self.__tac.deleteMediaTagByKey(self.__pattern.getId(), tag[0]):
self.updateTags()

View File

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

View File

@@ -1,6 +1,7 @@
import click import click
from ffx.model.show import Show from ffx.model.show import Show
from ffx.show_descriptor import ShowDescriptor
class ShowController(): class ShowController():
@@ -11,51 +12,65 @@ class ShowController():
self.Session = self.context['database']['session'] # convenience self.Session = self.context['database']['session'] # convenience
def getShowDesciptor(self, showId): def getShowDescriptor(self, showId):
try: try:
s = self.Session() s = self.Session()
q = s.query(Show).filter(Show.id == showId) q = s.query(Show).filter(Show.id == showId)
showDescriptor = {}
if q.count(): if q.count():
show = q.first() show: Show = q.first()
return show.getDescriptor()
# 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()
except Exception as ex: except Exception as ex:
raise click.ClickException(f"ShowController.getShowDesciptor(): {repr(ex)}") raise click.ClickException(f"ShowController.getShowDescriptor(): {repr(ex)}")
finally:
s.close()
def getShow(self, showId):
try:
s = self.Session()
q = s.query(Show).filter(Show.id == showId)
return q.first() if q.count() else None
except Exception as ex:
raise click.ClickException(f"ShowController.getShow(): {repr(ex)}")
finally:
s.close()
def getAllShows(self):
try:
s = self.Session()
q = s.query(Show)
if q.count():
return q.all()
else:
return []
except Exception as ex:
raise click.ClickException(f"ShowController.getAllShows(): {repr(ex)}")
finally: finally:
s.close() s.close()
def updateShow(self, showDescriptor): def updateShow(self, showDescriptor: ShowDescriptor):
try: try:
s = self.Session() s = self.Session()
q = s.query(Show).filter(Show.id == showDescriptor['id']) q = s.query(Show).filter(Show.id == showDescriptor.getId())
if not q.count(): if not q.count():
show = Show(id = int(showDescriptor['id']), show = Show(id = int(showDescriptor.getId()),
name = str(showDescriptor['name']), name = str(showDescriptor.getName()),
year = int(showDescriptor['year']), year = int(showDescriptor.getYear()),
index_season_digits = showDescriptor['index_season_digits'], index_season_digits = showDescriptor.getIndexSeasonDigits(),
index_episode_digits = showDescriptor['index_episode_digits'], index_episode_digits = showDescriptor.getIndexEpisodeDigits(),
indicator_season_digits = showDescriptor['indicator_season_digits'], indicator_season_digits = showDescriptor.getIndicatorSeasonDigits(),
indicator_episode_digits = showDescriptor['indicator_episode_digits']) indicator_episode_digits = showDescriptor.getIndicatorEpisodeDigits())
s.add(show) s.add(show)
s.commit() s.commit()
@@ -65,24 +80,24 @@ class ShowController():
currentShow = q.first() currentShow = q.first()
changed = False changed = False
if currentShow.name != str(showDescriptor['name']): if currentShow.name != str(showDescriptor.getName()):
currentShow.name = str(showDescriptor['name']) currentShow.name = str(showDescriptor.getName())
changed = True changed = True
if currentShow.year != int(showDescriptor['year']): if currentShow.year != int(showDescriptor.getYear()):
currentShow.year = int(showDescriptor['year']) currentShow.year = int(showDescriptor.getYear())
changed = True changed = True
if currentShow.index_season_digits != int(showDescriptor['index_season_digits']): if currentShow.index_season_digits != int(showDescriptor.getIndexSeasonDigits()):
currentShow.index_season_digits = int(showDescriptor['index_season_digits']) currentShow.index_season_digits = int(showDescriptor.getIndexSeasonDigits())
changed = True changed = True
if currentShow.index_episode_digits != int(showDescriptor['index_episode_digits']): if currentShow.index_episode_digits != int(showDescriptor.getIndexEpisodeDigits()):
currentShow.index_episode_digits = int(showDescriptor['index_episode_digits']) currentShow.index_episode_digits = int(showDescriptor.getIndexEpisodeDigits())
changed = True changed = True
if currentShow.indicator_season_digits != int(showDescriptor['indicator_season_digits']): if currentShow.indicator_season_digits != int(showDescriptor.getIndicatorSeasonDigits()):
currentShow.indicator_season_digits = int(showDescriptor['indicator_season_digits']) currentShow.indicator_season_digits = int(showDescriptor.getIndicatorSeasonDigits())
changed = True changed = True
if currentShow.indicator_episode_digits != int(showDescriptor['indicator_episode_digits']): if currentShow.indicator_episode_digits != int(showDescriptor.getIndicatorEpisodeDigits()):
currentShow.indicator_episode_digits = int(showDescriptor['indicator_episode_digits']) currentShow.indicator_episode_digits = int(showDescriptor.getIndicatorEpisodeDigits())
changed = True changed = True
if changed: if changed:

View File

@@ -1,12 +1,6 @@
import click
from textual import events
from textual.app import App, ComposeResult
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import Header, Footer, Placeholder, Label, ListView, ListItem, Static, DataTable, Button, Input from textual.widgets import Header, Footer, Static, Button
from textual.containers import Grid, Horizontal from textual.containers import Grid
from ffx.model.show import Show
from .show_controller import ShowController from .show_controller import ShowController
@@ -51,14 +45,13 @@ class ShowDeleteScreen(Screen):
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.__showDescriptor = self.__sc.getShowDescriptor(showId) if showId is not None else {}
def on_mount(self): def on_mount(self):
if not self.__showDescriptor is None:
if self.show_obj: self.query_one("#showlabel", Static).update(f"{self.__showDescriptor.getId()} - {self.__showDescriptor.getName()} ({self.__showDescriptor.getYear()})")
self.query_one("#showlabel", Static).update(f"{self.show_obj['id']} - {self.show_obj['name']} ({self.show_obj['year']})")
def compose(self): def compose(self):
@@ -90,8 +83,9 @@ class ShowDeleteScreen(Screen):
if event.button.id == "delete_button": if event.button.id == "delete_button":
if self.__sc.deleteShow(self.show_obj['id']): if not self.__showDescriptor is None:
self.dismiss(self.show_obj['id']) if self.__sc.deleteShow(self.__showDescriptor.getId()):
self.dismiss(self.__showDescriptor)
else: else:
#TODO: Meldung #TODO: Meldung

102
bin/ffx/show_descriptor.py Normal file
View File

@@ -0,0 +1,102 @@
import click
from typing import List, Self
from ffx.track_type import TrackType
from ffx.track_disposition import TrackDisposition
from ffx.track_descriptor import TrackDescriptor
from ffx.helper import dictDiff, DIFF_ADDED_KEY, DIFF_CHANGED_KEY, DIFF_REMOVED_KEY
class ShowDescriptor():
"""This class represents the structural content of a media file including streams and metadata"""
# CONTEXT_KEY = 'context'
ID_KEY = 'id'
NAME_KEY = 'name'
YEAR_KEY = 'year'
INDEX_SEASON_DIGITS_KEY = 'index_season_digits'
INDEX_EPISODE_DIGITS_KEY = 'index_episode_digits'
INDICATOR_SEASON_DIGITS_KEY = 'indicator_season_digits'
INDICATOR_EPISODE_DIGITS_KEY = 'indicator_episode_digits'
DEFAULT_INDEX_SEASON_DIGITS = 2
DEFAULT_INDEX_EPISODE_DIGITS = 2
DEFAULT_INDICATOR_SEASON_DIGITS = 2
DEFAULT_INDICATOR_EPISODE_DIGITS = 2
def __init__(self, **kwargs):
if ShowDescriptor.ID_KEY in kwargs.keys():
if type(kwargs[ShowDescriptor.ID_KEY]) is not int:
raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.ID_KEY} is required to be of type int")
self.__showId = kwargs[ShowDescriptor.ID_KEY]
else:
self.__showId = -1
if ShowDescriptor.NAME_KEY in kwargs.keys():
if type(kwargs[ShowDescriptor.NAME_KEY]) is not str:
raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.NAME_KEY} is required to be of type str")
self.__showName = kwargs[ShowDescriptor.NAME_KEY]
else:
self.__showName = ''
if ShowDescriptor.YEAR_KEY in kwargs.keys():
if type(kwargs[ShowDescriptor.YEAR_KEY]) is not int:
raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.YEAR_KEY} is required to be of type int")
self.__showYear = kwargs[ShowDescriptor.YEAR_KEY]
else:
self.__showYear = -1
if ShowDescriptor.INDEX_SEASON_DIGITS_KEY in kwargs.keys():
if type(kwargs[ShowDescriptor.INDEX_SEASON_DIGITS_KEY]) is not int:
raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDEX_SEASON_DIGITS_KEY} is required to be of type int")
self.__indexSeasonDigits = kwargs[ShowDescriptor.INDEX_SEASON_DIGITS_KEY]
else:
self.__indexSeasonDigits = -1
if ShowDescriptor.INDEX_EPISODE_DIGITS_KEY in kwargs.keys():
if type(kwargs[ShowDescriptor.INDEX_EPISODE_DIGITS_KEY]) is not int:
raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDEX_EPISODE_DIGITS_KEY} is required to be of type int")
self.__indexEpisodeDigits = kwargs[ShowDescriptor.INDEX_EPISODE_DIGITS_KEY]
else:
self.__indexEpisodeDigits = -1
if ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY in kwargs.keys():
if type(kwargs[ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY]) is not int:
raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY} is required to be of type int")
self.__indicatorSeasonDigits = kwargs[ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY]
else:
self.__indicatorSeasonDigits = -1
if ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY in kwargs.keys():
if type(kwargs[ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY]) is not int:
raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY} is required to be of type int")
self.__indicatorEpisodeDigits = kwargs[ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY]
else:
self.__indicatorEpisodeDigits = -1
def getId(self):
return self.__showId
def getName(self):
return self.__showName
def getYear(self):
return self.__showYear
def getIndexSeasonDigits(self):
return self.__indexSeasonDigits
def getIndexEpisodeDigits(self):
return self.__indexEpisodeDigits
def getIndicatorSeasonDigits(self):
return self.__indicatorSeasonDigits
def getIndicatorEpisodeDigits(self):
return self.__indicatorEpisodeDigits
def getFilenamePrefix(self):
return f"{self.__showName} ({str(self.__showYear)})"

View File

@@ -1,10 +1,9 @@
import click import click
from datetime import datetime
from textual import events
from textual.app import App, ComposeResult
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import Header, Footer, Placeholder, Label, ListView, ListItem, Static, DataTable, Button, Input from textual.widgets import Header, Footer, Static, Button, DataTable, Input
from textual.containers import Grid, Horizontal from textual.containers import Grid
from textual.widgets._data_table import CellDoesNotExist from textual.widgets._data_table import CellDoesNotExist
@@ -16,6 +15,12 @@ from .pattern_delete_screen import PatternDeleteScreen
from .show_controller import ShowController from .show_controller import ShowController
from .pattern_controller import PatternController from .pattern_controller import PatternController
from .tmdb_controller import TmdbController
from .show_descriptor import ShowDescriptor
from .helper import filterFilename
# Screen[dict[int, str, int]] # Screen[dict[int, str, int]]
class ShowDetailsScreen(Screen): class ShowDetailsScreen(Screen):
@@ -80,38 +85,43 @@ class ShowDetailsScreen(Screen):
self.__sc = ShowController(context = self.context) self.__sc = ShowController(context = self.context)
self.__pc = PatternController(context = self.context) self.__pc = PatternController(context = self.context)
self.__tc = TmdbController()
self.show_obj = self.__sc.getShowDesciptor(showId) if showId is not None else {} self.__showDescriptor = self.__sc.getShowDescriptor(showId) if showId is not None else None
def loadPatterns(self, show_id): def loadPatterns(self, show_id : int):
try: try:
s = self.Session() s = self.Session()
q = s.query(Pattern).filter(Pattern.show_id == int(show_id)) q = s.query(Pattern).filter(Pattern.show_id == int(show_id))
return [{'id': int(p.id), 'pattern': p.pattern} for p in q.all()] return [{'id': int(p.id), 'pattern': str(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"ShowDetailsScreen.loadPatterns(): {repr(ex)}")
finally: finally:
s.close() s.close()
def on_mount(self): def on_mount(self):
if self.show_obj: if not self.__showDescriptor is None:
self.query_one("#id_wdg", Static).update(str(self.show_obj['id'])) self.query_one("#id_static", Static).update(str(self.__showDescriptor.getId()))
self.query_one("#name_input", Input).value = str(self.show_obj['name']) self.query_one("#name_input", Input).value = str(self.__showDescriptor.getName())
self.query_one("#year_input", Input).value = str(self.show_obj['year']) self.query_one("#year_input", Input).value = str(self.__showDescriptor.getYear())
self.query_one("#index_season_digits_input", Input).value = str(self.show_obj['index_season_digits']) self.query_one("#index_season_digits_input", Input).value = str(self.__showDescriptor.getIndexSeasonDigits())
self.query_one("#index_episode_digits_input", Input).value = str(self.show_obj['index_episode_digits']) self.query_one("#index_episode_digits_input", Input).value = str(self.__showDescriptor.getIndexEpisodeDigits())
self.query_one("#indicator_season_digits_input", Input).value = str(self.show_obj['indicator_season_digits']) self.query_one("#indicator_season_digits_input", Input).value = str(self.__showDescriptor.getIndicatorSeasonDigits())
self.query_one("#indicator_episode_digits_input", Input).value = str(self.show_obj['indicator_episode_digits']) self.query_one("#indicator_episode_digits_input", Input).value = str(self.__showDescriptor.getIndicatorEpisodeDigits())
for pattern in self.loadPatterns(int(self.show_obj['id'])): showId = int(self.__showDescriptor.getId())
#raise click.ClickException(f"show_id {showId}")
patternList = self.loadPatterns(showId)
# raise click.ClickException(f"patternList {patternList}")
for pattern in patternList:
row = (pattern['pattern'],) row = (pattern['pattern'],)
self.patternTable.add_row(*map(str, row)) self.patternTable.add_row(*map(str, row))
@@ -136,7 +146,7 @@ class ShowDetailsScreen(Screen):
if row_key is not None: if row_key is not None:
selected_row_data = self.patternTable.get_row(row_key) selected_row_data = self.patternTable.get_row(row_key)
selectedPattern['show_id'] = self.show_obj['id'] selectedPattern['show_id'] = self.__showDescriptor.getId()
selectedPattern['pattern'] = str(selected_row_data[0]) selectedPattern['pattern'] = str(selected_row_data[0])
except CellDoesNotExist: except CellDoesNotExist:
@@ -149,8 +159,8 @@ class ShowDetailsScreen(Screen):
def action_add_pattern(self): def action_add_pattern(self):
if self.show_obj: if not self.__showDescriptor is None:
self.app.push_screen(PatternDetailsScreen(showId = self.show_obj['id']), self.handle_add_pattern) # <- self.app.push_screen(PatternDetailsScreen(showId = self.__showDescriptor.getId()), self.handle_add_pattern) # <-
def handle_add_pattern(self, screenResult): def handle_add_pattern(self, screenResult):
@@ -168,9 +178,9 @@ class ShowDetailsScreen(Screen):
selectedPatternId = self.__pc.findPattern(selectedPatternDescriptor) selectedPatternId = self.__pc.findPattern(selectedPatternDescriptor)
if selectedPatternId is None: if selectedPatternId is None:
raise click.ClickException(f"ShowDetailsScreen.action_edit_pattern(): Pattern to remove has no id") raise click.ClickException(f"ShowDetailsScreen.action_edit_pattern(): Pattern to edit has no id")
self.app.push_screen(PatternDetailsScreen(patternId = selectedPatternId, showId = self.show_obj['id']), self.handle_edit_pattern) # <- self.app.push_screen(PatternDetailsScreen(patternId = selectedPatternId, showId = self.__showDescriptor.getId()), self.handle_edit_pattern) # <-
def handle_edit_pattern(self, screenResult): def handle_edit_pattern(self, screenResult):
@@ -195,7 +205,7 @@ class ShowDetailsScreen(Screen):
if selectedPatternId is None: if selectedPatternId is None:
raise click.ClickException(f"ShowDetailsScreen.action_remove_pattern(): Pattern to remove has no id") raise click.ClickException(f"ShowDetailsScreen.action_remove_pattern(): Pattern to remove has no id")
self.app.push_screen(PatternDeleteScreen(patternId = selectedPatternId, showId = self.show_obj['id']), self.handle_remove_pattern) self.app.push_screen(PatternDeleteScreen(patternId = selectedPatternId, showId = self.__showDescriptor.getId()), self.handle_remove_pattern)
def handle_remove_pattern(self, screenResult): def handle_remove_pattern(self, screenResult):
@@ -215,25 +225,24 @@ class ShowDetailsScreen(Screen):
# Define the columns with headers # Define the columns with headers
self.column_key_pattern = self.patternTable.add_column("Pattern", width=150) self.column_key_pattern = self.patternTable.add_column("Pattern", width=150)
#self.column_key_name = self.patternTable.add_column("Name", width=50)
#self.column_key_year = self.patternTable.add_column("Year", width=10)
self.patternTable.cursor_type = 'row' self.patternTable.cursor_type = 'row'
yield Header() yield Header()
with Grid(): with Grid():
# 1 # 1
yield Static("Show" if self.show_obj else "New Show", id="toplabel", classes="five") yield Static("Show" if not self.__showDescriptor is None else "New Show", id="toplabel")
yield Button("Identify", id="identify_button")
yield Static(" ", classes="three")
# 2 # 2
yield Static("ID") yield Static("ID")
if self.show_obj: if not self.__showDescriptor is None:
yield Static("", id="id_wdg", classes="four") yield Static("", id="id_static", classes="four")
else: else:
yield Input(type="integer", id="id_wdg", classes="four") yield Input(type="integer", id="id_input", classes="four")
# 3 # 3
yield Static("Name") yield Static("Name")
@@ -283,22 +292,44 @@ class ShowDetailsScreen(Screen):
def getShowDescriptorFromInput(self): def getShowDescriptorFromInput(self):
showDescriptor = {} kwargs = {}
if self.show_obj:
showDescriptor['id'] = int(self.show_obj['id'])
else:
showDescriptor['id'] = int(self.query_one("#id_wdg", Input).value)
showDescriptor['name'] = str(self.query_one("#name_input", Input).value)
showDescriptor['year'] = int(self.query_one("#year_input", Input).value)
showDescriptor['index_season_digits'] = int(self.query_one("#index_season_digits_input", Input).value) try:
showDescriptor['index_episode_digits'] = int(self.query_one("#index_episode_digits_input", Input).value) if self.__showDescriptor:
showDescriptor['indicator_season_digits'] = int(self.query_one("#indicator_season_digits_input", Input).value) kwargs[ShowDescriptor.ID_KEY] = int(self.__showDescriptor.getId())
showDescriptor['indicator_episode_digits'] = int(self.query_one("#indicator_episode_digits_input", Input).value) else:
kwargs[ShowDescriptor.ID_KEY] = int(self.query_one("#id_input", Input).value)
except ValueError:
return None
return showDescriptor try:
kwargs[ShowDescriptor.NAME_KEY] = str(self.query_one("#name_input", Input).value)
except ValueError:
pass
try:
kwargs[ShowDescriptor.YEAR_KEY] = int(self.query_one("#year_input", Input).value)
except ValueError:
pass
try:
kwargs[ShowDescriptor.INDEX_SEASON_DIGITS_KEY] = int(self.query_one("#index_season_digits_input", Input).value)
except ValueError:
pass
try:
kwargs[ShowDescriptor.INDEX_EPISODE_DIGITS_KEY] = int(self.query_one("#index_episode_digits_input", Input).value)
except ValueError:
pass
try:
kwargs[ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY] = int(self.query_one("#indicator_season_digits_input", Input).value)
except ValueError:
pass
try:
kwargs[ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY] = int(self.query_one("#indicator_episode_digits_input", Input).value)
except ValueError:
pass
return ShowDescriptor(**kwargs)
# Event handler for button press # Event handler for button press
@@ -308,12 +339,23 @@ class ShowDetailsScreen(Screen):
showDescriptor = self.getShowDescriptorFromInput() showDescriptor = self.getShowDescriptorFromInput()
if self.__sc.updateShow(showDescriptor): if not showDescriptor is None:
self.dismiss(showDescriptor) if self.__sc.updateShow(showDescriptor):
else: self.dismiss(showDescriptor)
#TODO: Meldung else:
self.app.pop_screen() #TODO: Meldung
self.app.pop_screen()
if event.button.id == "cancel_button": if event.button.id == "cancel_button":
self.app.pop_screen() self.app.pop_screen()
if event.button.id == "identify_button":
showDescriptor = self.getShowDescriptorFromInput()
if not showDescriptor is None:
showResult = self.__tc.queryShow(showDescriptor.getId())
firstAirDate = datetime.strptime(showResult['first_air_date'], '%Y-%m-%d')
self.query_one("#name_input", Input).value = filterFilename(showResult['name'])
self.query_one("#year_input", Input).value = str(firstAirDate.year)

View File

@@ -1,15 +1,18 @@
import click import click
from textual.app import App, ComposeResult
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import Header, Footer, Placeholder, Label, ListView, ListItem, Static, DataTable, Button from textual.widgets import Header, Footer, Placeholder, Label, ListView, ListItem, Static, DataTable, Button
from textual.containers import Grid, Horizontal from textual.containers import Grid
from ffx.model.show import Show from ffx.model.show import Show
from .show_controller import ShowController
from .show_details_screen import ShowDetailsScreen from .show_details_screen import ShowDetailsScreen
from .show_delete_screen import ShowDeleteScreen from .show_delete_screen import ShowDeleteScreen
from ffx.show_descriptor import ShowDescriptor
from .help_screen import HelpScreen from .help_screen import HelpScreen
from textual.widgets._data_table import CellDoesNotExist from textual.widgets._data_table import CellDoesNotExist
@@ -56,9 +59,9 @@ class ShowsScreen(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)
def getSelectedShowId(self): def getSelectedShowId(self):
@@ -96,14 +99,14 @@ class ShowsScreen(Screen):
self.app.push_screen(ShowDetailsScreen(showId = selectedShowId), self.handle_edit_screen) self.app.push_screen(ShowDetailsScreen(showId = selectedShowId), self.handle_edit_screen)
def handle_edit_screen(self, screenResult): def handle_edit_screen(self, showDescriptor: ShowDescriptor):
try: try:
row_key, col_key = self.table.coordinate_to_cell_key(self.table.cursor_coordinate) row_key, col_key = self.table.coordinate_to_cell_key(self.table.cursor_coordinate)
self.table.update_cell(row_key, self.column_key_name, screenResult['name']) self.table.update_cell(row_key, self.column_key_name, showDescriptor.getName())
self.table.update_cell(row_key, self.column_key_year, screenResult['year']) self.table.update_cell(row_key, self.column_key_year, showDescriptor.getYear())
except CellDoesNotExist: except CellDoesNotExist:
pass pass
@@ -119,7 +122,7 @@ class ShowsScreen(Screen):
self.app.push_screen(ShowDeleteScreen(showId = selectedShowId), self.handle_delete_show) self.app.push_screen(ShowDeleteScreen(showId = selectedShowId), self.handle_delete_show)
def handle_delete_show(self, screenResult): def handle_delete_show(self, showDescriptor: ShowDescriptor):
try: try:
row_key, col_key = self.table.coordinate_to_cell_key(self.table.cursor_coordinate) row_key, col_key = self.table.coordinate_to_cell_key(self.table.cursor_coordinate)
@@ -129,24 +132,10 @@ class ShowsScreen(Screen):
pass pass
def loadShows(self):
try:
s = self.Session()
q = s.query(Show)
return [(int(s.id), s.name, s.year) for s in q.all()]
except Exception as ex:
raise click.ClickException(f"ShowsScreen.loadShows(): {repr(ex)}")
finally:
s.close()
def on_mount(self) -> None: def on_mount(self) -> None:
for show in self.loadShows(): for show in self.__sc.getAllShows():
self.table.add_row(*map(str, show)) # Convert each element to a string before adding row = (int(show.id), show.name, show.year) # Convert each element to a string before adding
self.table.add_row(*map(str, row))
def compose(self): def compose(self):

View File

@@ -2,11 +2,6 @@ import click
from ffx.model.track import Track 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.media_tag import MediaTag
from ffx.model.track_tag import TrackTag from ffx.model.track_tag import TrackTag
@@ -19,175 +14,207 @@ class TagController():
self.Session = self.context['database']['session'] # convenience self.Session = self.context['database']['session'] # convenience
def addMediaTag(self, trackDescriptor): def updateMediaTag(self, patternId, tagKey, tagValue):
try: try:
s = self.Session() s = self.Session()
track = Track(pattern_id = int(trackDescriptor['pattern_id']), q = s.query(MediaTag).filter(MediaTag.pattern_id == int(patternId),
MediaTag.key == str(tagKey))
track_type = int(trackDescriptor['type'].value), tag = q.first()
if tag:
sub_index = int(trackDescriptor['sub_index']), tag.value = str(tagValue)
else:
# language = str(trackDescriptor['language'].threeLetter()), tag = MediaTag(pattern_id = int(patternId),
# title = str(trackDescriptor['title']), key = str(tagKey),
value = str(tagValue))
disposition_flags = int(TrackDisposition.toFlags(trackDescriptor['disposition_list']))) s.add(tag)
s.add(track)
s.commit() s.commit()
return int(tag.id)
except Exception as ex: except Exception as ex:
raise click.ClickException(f"TrackController.addTrack(): {repr(ex)}") raise click.ClickException(f"TagController.updateTrackTag(): {repr(ex)}")
finally: finally:
s.close() s.close()
def addTrackTag(self, trackDescriptor): def updateTrackTag(self, trackId, tagKey, tagValue):
try: try:
s = self.Session() s = self.Session()
track = Track(pattern_id = int(trackDescriptor['pattern_id']), q = s.query(TrackTag).filter(TrackTag.track_id == int(trackId),
TrackTag.key == str(tagKey))
track_type = int(trackDescriptor['type'].value), tag = q.first()
if tag:
sub_index = int(trackDescriptor['sub_index']), tag.value = str(tagValue)
else:
# language = str(trackDescriptor['language'].threeLetter()), tag = TrackTag(track_id = int(trackId),
# title = str(trackDescriptor['title']), key = str(tagKey),
value = str(tagValue))
disposition_flags = int(TrackDisposition.toFlags(trackDescriptor['disposition_list']))) s.add(tag)
s.add(track)
s.commit() s.commit()
return int(tag.id)
except Exception as ex: except Exception as ex:
raise click.ClickException(f"TrackController.addTrack(): {repr(ex)}") raise click.ClickException(f"TagController.updateTrackTag(): {repr(ex)}")
finally: finally:
s.close() s.close()
def deleteMediaTagByKey(self, patternId, tagKey):
def updateTrack(self, trackId, trackDescriptor):
try: try:
s = self.Session() s = self.Session()
q = s.query(Track).filter(Track.id == int(trackId))
q = s.query(MediaTag).filter(MediaTag.pattern_id == int(patternId),
MediaTag.key == str(tagKey))
if q.count(): if q.count():
tag = q.first()
track = q.first() s.delete(tag)
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() s.commit()
return True return True
else: else:
return False return False
except Exception as ex: except Exception as ex:
raise click.ClickException(f"TrackController.addTrack(): {repr(ex)}") raise click.ClickException(f"TagController.deleteMediaTagByKey(): {repr(ex)}")
finally: finally:
s.close() s.close()
def deleteTrackTagByKey(self, trackId, tagKey):
def findAllTracks(self, patternId):
try: try:
s = self.Session() s = self.Session()
trackDescriptors = {} q = s.query(TrackTag).filter(TrackTag.track_id == int(trackId),
trackDescriptors[TrackType.AUDIO.label()] = [] TrackTag.key == str(tagKey))
trackDescriptors[TrackType.SUBTITLE.label()] = [] tag = q.first()
if tag:
q_audio = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == TrackType.AUDIO.index()) s.delete(tag)
for audioTrack in q_audio.all(): s.commit()
trackDescriptors[TrackType.AUDIO.label()].append(audioTrack.id) return True
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: else:
return None return False
except Exception as ex: except Exception as ex:
raise click.ClickException(f"TrackController.findTrack(): {repr(ex)}") raise click.ClickException(f"TagController.deleteTrackTagByKey(): {repr(ex)}")
finally: finally:
s.close() s.close()
def findAllMediaTags(self, patternId) -> dict:
def getTrackDescriptor(self, trackId):
try: try:
s = self.Session() s = self.Session()
q = s.query(Track).filter(Track.id == int(trackId))
q = s.query(MediaTag).filter(MediaTag.pattern_id == int(patternId))
if q.count(): if q.count():
track = q.first() return {t.key:t.value for t in q.all()}
#return self.getTrackDict(track)
return track.getDescriptor()
else: else:
return {} return {}
except Exception as ex: except Exception as ex:
raise click.ClickException(f"TrackController.getTrackDescriptor(): {repr(ex)}") raise click.ClickException(f"TagController.findAllMediaTags(): {repr(ex)}")
finally: finally:
s.close() s.close()
def deleteTrack(self, trackId): def findAllTrackTags(self, trackId) -> dict:
try: try:
s = self.Session() s = self.Session()
q = s.query(Track).filter(Track.id == int(trackId))
q = s.query(TrackTag).filter(TrackTag.track_id == int(trackId))
if q.count():
return {t.key:t.value for t in q.all()}
else:
return {}
except Exception as ex:
raise click.ClickException(f"TagController.findAllTracks(): {repr(ex)}")
finally:
s.close()
def findMediaTag(self, trackId : int, trackKey : str) -> MediaTag:
try:
s = self.Session()
q = s.query(Track).filter(MediaTag.track_id == int(trackId), MediaTag.key == str(trackKey))
if q.count():
return q.first()
else:
return None
except Exception as ex:
raise click.ClickException(f"TagController.findMediaTag(): {repr(ex)}")
finally:
s.close()
def findTrackTag(self, trackId : int, tagKey : str) -> TrackTag:
try:
s = self.Session()
q = s.query(TrackTag).filter(TrackTag.track_id == int(trackId), TrackTag.key == str(tagKey))
if q.count():
return q.first()
else:
return None
except Exception as ex:
raise click.ClickException(f"TagController.findTrackTag(): {repr(ex)}")
finally:
s.close()
def deleteMediaTag(self, tagId) -> bool:
try:
s = self.Session()
q = s.query(MediaTag).filter(MediaTag.id == int(tagId))
if q.count(): if q.count():
trackDescriptor = self.getTrackDict(q.first()) tag = q.first()
s.delete(tag)
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() s.commit()
return True return True
return False
except Exception as ex:
raise click.ClickException(f"TagController.deleteMediaTag(): {repr(ex)}")
finally:
s.close()
def deleteTrackTag(self, tagId : int) -> bool:
if type(tagId) is not int:
raise TypeError('TagController.deleteTrackTag(): Argument tagId is required to be of type int')
try:
s = self.Session()
q = s.query(TrackTag).filter(TrackTag.id == int(tagId))
if q.count():
tag = q.first()
s.delete(tag)
s.commit()
return True
return False return False
except Exception as ex: except Exception as ex:
raise click.ClickException(f"TrackController.deleteTrack(): {repr(ex)}") raise click.ClickException(f"TagController.deleteTrackTag(): {repr(ex)}")
finally: finally:
s.close() s.close()

View File

@@ -0,0 +1,98 @@
from textual.screen import Screen
from textual.widgets import Header, Footer, Static, Button
from textual.containers import Grid
# Screen[dict[int, str, int]]
class TagDeleteScreen(Screen):
CSS = """
Grid {
grid-size: 4 9;
grid-rows: 2 2 2 2 2 2 2 2 2;
grid-columns: 30 30 30 30;
height: 100%;
width: 100%;
padding: 1;
}
Input {
border: none;
}
Button {
border: none;
}
#toplabel {
height: 1;
}
.two {
column-span: 2;
}
.three {
column-span: 3;
}
.four {
column-span: 4;
}
.five {
column-span: 5;
}
.box {
height: 100%;
border: solid green;
}
"""
def __init__(self, key=None, value=None):
super().__init__()
self.__key = key
self.__value = value
def on_mount(self):
self.query_one("#keylabel", Static).update(str(self.__key))
self.query_one("#valuelabel", Static).update(str(self.__value))
def compose(self):
yield Header()
with Grid():
#1
yield Static(f"Are you sure to delete this tag ?", id="toplabel", classes="five")
#2
yield Static("Key")
yield Static(" ", id="keylabel", classes="four")
#3
yield Static("Value")
yield Static(" ", id="valuelabel", classes="four")
#4
yield Static(" ", classes="five")
#9
yield Button("Delete", id="delete_button")
yield Button("Cancel", id="cancel_button")
yield Footer()
# Event handler for button press
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "delete_button":
tag = (self.__key, self.__value)
self.dismiss(tag)
if event.button.id == "cancel_button":
self.app.pop_screen()

View File

@@ -0,0 +1,121 @@
from textual.screen import Screen
from textual.widgets import Header, Footer, Static, Button, Input
from textual.containers import Grid
# Screen[dict[int, str, int]]
class TagDetailsScreen(Screen):
CSS = """
Grid {
grid-size: 5 20;
grid-rows: 2 2 2 2 2 3 2 2 2 2 2 6 2 2 6 2 2 2 2 6;
grid-columns: 25 25 25 25 225;
height: 100%;
width: 100%;
padding: 1;
}
Input {
border: none;
}
Button {
border: none;
}
SelectionList {
border: none;
min-height: 6;
}
Select {
border: none;
}
DataTable {
min-height: 6;
}
#toplabel {
height: 1;
}
.two {
column-span: 2;
}
.three {
column-span: 3;
}
.four {
column-span: 4;
}
.five {
column-span: 5;
}
.box {
height: 100%;
border: solid green;
}
"""
def __init__(self, key=None, value=None):
super().__init__()
self.__key = key
self.__value = value
def on_mount(self):
if self.__key is not None:
self.query_one("#key_input", Input).value = str(self.__key)
if self.__value is not None:
self.query_one("#value_input", Input).value = str(self.__value)
def compose(self):
yield Header()
with Grid():
# 8
yield Static("Key")
yield Input(id="key_input", classes="four")
yield Static("Value")
yield Input(id="value_input", classes="four")
# 17
yield Static(" ", classes="five")
# 18
yield Button("Save", id="save_button")
yield Button("Cancel", id="cancel_button")
# 19
yield Static(" ", classes="five")
# 20
yield Static(" ", classes="five", id="messagestatic")
yield Footer(id="footer")
def getTagFromInput(self):
tagKey = self.query_one("#key_input", Input).value
tagValue = self.query_one("#value_input", Input).value
return (tagKey, tagValue)
# 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":
self.dismiss(self.getTagFromInput())
if event.button.id == "cancel_button":
self.app.pop_screen()

View File

@@ -14,8 +14,54 @@ class TmdbController():
self.tmdbLanguage = TmdbController.DEFAULT_LANGUAGE self.tmdbLanguage = TmdbController.DEFAULT_LANGUAGE
def queryShow(self, showId):
"""
First level keys in the response object:
adult bool
backdrop_path str
created_by []
episode_run_time []
first_air_date str YYYY-MM-DD
genres []
homepage str
id int
in_production bool
languages []
last_air_date str YYYY-MM-DD
last_episode_to_air {}
name str
next_episode_to_air null
networks []
number_of_episodes int
number_of_seasons int
origin_country []
original_language str
original_name str
overview str
popularity float
poster_path str
production_companies []
production_countries []
seasons []
spoken_languages []
status str
tagline str
type str
vote_average float
vote_count int
"""
urlParams = f"?language={self.tmdbLanguage}&api_key={self.__tmdbApiKey}"
tmdbUrl = f"https://api.themoviedb.org/3/tv/{showId}W{urlParams}"
def queryTmdb(self, showId, season, episode): #TODO Check for result
try:
return requests.get(tmdbUrl).json()
except:
return {}
def queryEpisode(self, showId, season, episode):
""" """
First level keys in the response object: First level keys in the response object:
air_date str 'YYY-MM-DD' air_date str 'YYY-MM-DD'
@@ -37,15 +83,17 @@ class TmdbController():
tmdbUrl = f"https://api.themoviedb.org/3/tv/{showId}/season/{season}/episode/{episode}{urlParams}" tmdbUrl = f"https://api.themoviedb.org/3/tv/{showId}/season/{season}/episode/{episode}{urlParams}"
return requests.get(tmdbUrl).json() #TODO Check for result
try:
return requests.get(tmdbUrl).json()
except:
return {}
def getEpisodeFileBasename(self,
def getEpisodeFilename(self,
showName, showName,
episodeName, episodeName,
season, season,
episode, episode,
extension,
indexSeasonDigits = 2, indexSeasonDigits = 2,
indexEpisodeDigits = 2, indexEpisodeDigits = 2,
indicatorSeasonDigits = 2, indicatorSeasonDigits = 2,
@@ -92,7 +140,5 @@ class TmdbController():
filenameTokens += ['S{num:{fill}{width}}'.format(num=season, fill='0', width=indicatorSeasonDigits)] filenameTokens += ['S{num:{fill}{width}}'.format(num=season, fill='0', width=indicatorSeasonDigits)]
if indicatorEpisodeDigits: if indicatorEpisodeDigits:
filenameTokens += ['E{num:{fill}{width}}'.format(num=episode, fill='0', width=indicatorEpisodeDigits)] filenameTokens += ['E{num:{fill}{width}}'.format(num=episode, fill='0', width=indicatorEpisodeDigits)]
filenameTokens += ['.', extension]
return ''.join(filenameTokens) return ''.join(filenameTokens)

View File

@@ -10,6 +10,7 @@ from .iso_language import IsoLanguage
from .track_type import TrackType from .track_type import TrackType
from ffx.model.track_tag import TrackTag from ffx.model.track_tag import TrackTag
from ffx.track_descriptor import TrackDescriptor
class TrackController(): class TrackController():
@@ -20,15 +21,19 @@ class TrackController():
self.Session = self.context['database']['session'] # convenience self.Session = self.context['database']['session'] # convenience
def addTrack(self, trackDescriptor): def addTrack(self, trackDescriptor : TrackDescriptor, patternId = None):
# option to override pattern id in case track descriptor has not set it
patId = int(trackDescriptor.getPatternId() if patternId is None else patternId)
try: try:
s = self.Session() s = self.Session()
track = Track(pattern_id = patId,
track = Track(pattern_id = int(trackDescriptor.getPatternId()),
track_type = int(trackDescriptor.getType().index()), track_type = int(trackDescriptor.getType().index()),
sub_index = int(trackDescriptor.getSubIndex()), index = int(trackDescriptor.getIndex()),
disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet()))) source_index = int(trackDescriptor.getSourceIndex()),
disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet())),
audio_layout = trackDescriptor.getAudioLayout().index())
s.add(track) s.add(track)
s.commit() s.commit()
@@ -47,7 +52,10 @@ class TrackController():
s.close() s.close()
def updateTrack(self, trackId, trackDescriptor): def updateTrack(self, trackId, trackDescriptor : TrackDescriptor):
if type(trackDescriptor) is not TrackDescriptor:
raise TypeError('TrackController.updateTrack(): Argument trackDescriptor is required to be of type TrackDescriptor')
try: try:
s = self.Session() s = self.Session()
@@ -55,50 +63,65 @@ class TrackController():
if q.count(): if q.count():
track = q.first() track : Track = q.first()
track.track_type = int(trackDescriptor.getType().index())
track.audio_layout = int(trackDescriptor.getAudioLayout().index())
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.getDispositionSet())) track.disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet()))
s.commit() descriptorTagKeys = trackDescriptor.getTags()
tagKeysInDescriptor = set(descriptorTagKeys.keys())
tagKeysInDb = {t.key for t in track.track_tags}
for k in tagKeysInDescriptor & tagKeysInDb: # to update
tags = [t for t in track.track_tags if t.key == k]
tags[0].value = descriptorTagKeys[k]
for k in tagKeysInDescriptor - tagKeysInDb: # to add
tag = TrackTag(track_id=track.id, key=k, value=descriptorTagKeys[k])
s.add(tag)
for k in tagKeysInDb - tagKeysInDescriptor: # to remove
tags = [t for t in track.track_tags if t.key == k]
s.delete(tags[0])
s.commit()
return True return True
else: else:
return False return False
except Exception as ex: except Exception as ex:
raise click.ClickException(f"TrackController.addTrack(): {repr(ex)}") raise click.ClickException(f"TrackController.updateTrack(): {repr(ex)}")
finally:
s.close()
def findTracks(self, patternId):
try:
s = self.Session()
q = s.query(Track).filter(Track.pattern_id == int(patternId))
return [a for a in q.all()]
except Exception as ex:
raise click.ClickException(f"TrackController.findTracks(): {repr(ex)}")
finally:
s.close()
#TODO: mit optionalem Parameter lösen ^
def findVideoTracks(self, patternId):
try:
s = self.Session()
q = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == TrackType.VIDEO.index())
return [a for a in q.all()]
except Exception as ex:
raise click.ClickException(f"TrackController.findVideoTracks(): {repr(ex)}")
finally: finally:
s.close() 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 findAudioTracks(self, patternId): def findAudioTracks(self, patternId):
try: try:
@@ -126,53 +149,90 @@ class TrackController():
s.close() s.close()
def findTrack(self, patternId : int, trackType : TrackType, subIndex : int): def getTrack(self, patternId : int, index: int) -> Track:
try: try:
s = self.Session() s = self.Session()
q = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == trackType.index(), Track.sub_index == int(subIndex)) q = s.query(Track).filter(Track.pattern_id == int(patternId), Track.index == int(index))
if q.count(): if q.count():
#track = q.first()
#return int(track.id)
return q.first() return q.first()
else: else:
return None return None
except Exception as ex: except Exception as ex:
raise click.ClickException(f"TrackController.findTrack(): {repr(ex)}") raise click.ClickException(f"TrackController.getTrack(): {repr(ex)}")
finally:
s.close()
def setDispositionState(self, patternId: int, index: int, disposition : TrackDisposition, state : bool):
if type(patternId) is not int:
raise TypeError('TrackController.setTrackDisposition(): Argument patternId is required to be of type int')
if type(index) is not int:
raise TypeError('TrackController.setTrackDisposition(): Argument index is required to be of type int')
if type(disposition) is not TrackDisposition:
raise TypeError('TrackController.setTrackDisposition(): Argument disposition is required to be of type TrackDisposition')
if type(state) is not bool:
raise TypeError('TrackController.setTrackDisposition(): Argument state is required to be of type bool')
try:
s = self.Session()
q = s.query(Track).filter(Track.pattern_id == patternId, Track.index == index)
if q.count():
track : Track = q.first()
if state:
track.setDisposition(disposition)
else:
track.resetDisposition(disposition)
s.commit()
return True
else:
return False
except Exception as ex:
raise click.ClickException(f"TrackController.updateTrack(): {repr(ex)}")
finally: finally:
s.close() s.close()
def deleteTrack(self, trackId): def deleteTrack(self, trackId):
try: try:
s = self.Session() s = self.Session()
q = s.query(Track).filter(Track.id == int(trackId)) q = s.query(Track).filter(Track.id == int(trackId))
if q.count(): if q.count():
patternId = int(q.first().pattern_id)
#trackDescriptor = self.getTrackDict(q.first()) q_siblings = s.query(Track).filter(Track.pattern_id == patternId).order_by(Track.index)
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) index = 0
subIndex = 0
for track in q_siblings.all(): for track in q_siblings.all():
if track.sub_index == track.getSubIndex(): if track.id == int(trackId):
s.delete(track) s.delete(track)
else: else:
track.sub_index = subIndex track.index = index
subIndex += 1 index += 1
s.commit() s.commit()
return True return True
return False return False
except Exception as ex: except Exception as ex:
raise click.ClickException(f"TrackController.deleteTrack(): {repr(ex)}") raise click.ClickException(f"TrackController.deleteTrack(): {repr(ex)}")
finally: finally:
s.close() s.close()
def setDefaultSubTrack(self, trackType, subIndex):
pass
def setForcedSubTrack(self, trackType, subIndex):
pass

View File

@@ -3,14 +3,13 @@ import click
from textual import events from textual import events
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import Header, Footer, Placeholder, Label, ListView, ListItem, Static, DataTable, Button, Input from textual.widgets import Header, Footer, Static, Button
from textual.containers import Grid, Horizontal from textual.containers import Grid
from ffx.model.pattern import Pattern from ffx.model.pattern import Pattern
from ffx.model.track import Track
from ffx.track_descriptor import TrackDescriptor from ffx.track_descriptor import TrackDescriptor
# from .show_controller import ShowController
# from .pattern_controller import PatternController
from .track_controller import TrackController from .track_controller import TrackController
from .track_type import TrackType from .track_type import TrackType
@@ -123,7 +122,7 @@ class TrackDeleteScreen(Screen):
if event.button.id == "delete_button": if event.button.id == "delete_button":
track = self.__tc.findTrack(self.__trackDescriptor.getPatternId(), self.__trackDescriptor.getType(), self.__trackDescriptor.getSubIndex()) track = self.__tc.getTrack(self.__trackDescriptor.getPatternId(), self.__trackDescriptor.getIndex())
if track is None: if track is None:
raise click.ClickException(f"Track is none: patternId={self.__trackDescriptor.getPatternId()} type={self.__trackDescriptor.getType()} subIndex={self.__trackDescriptor.getSubIndex()}") raise click.ClickException(f"Track is none: patternId={self.__trackDescriptor.getPatternId()} type={self.__trackDescriptor.getType()} subIndex={self.__trackDescriptor.getSubIndex()}")

View File

@@ -1,170 +1,294 @@
import click
from .iso_language import IsoLanguage from .iso_language import IsoLanguage
from .track_type import TrackType from .track_type import TrackType
from .audio_layout import AudioLayout from .audio_layout import AudioLayout
from .track_disposition import TrackDisposition from .track_disposition import TrackDisposition
from .helper import dictDiff, setDiff
class TrackDescriptor():
INDEX_KEY = 'index'
SUB_INDEX_KEY = 'sub_index'
PATTERN_ID_KEY = 'pattern_id'
TRACK_TYPE_KEY = 'track_type'
DISPOSITION_SET_KEY = 'disposition_set'
TAGS_KEY = 'tags'
AUDIO_LAYOUT_KEY = 'audio_layout'
FFPROBE_DISPOSITION_KEY = 'disposition'
FFPROBE_TAGS_KEY = 'tags'
def __init__(self, **kwargs):
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(): class TrackDescriptor:
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(): ID_KEY = "id"
if type(kwargs[TrackDescriptor.TAGS_KEY]) is not dict: INDEX_KEY = "index"
raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.TAGS_KEY} is required to be of type dict") SOURCE_INDEX_KEY = "source_index"
self.__trackTags = kwargs[TrackDescriptor.TAGS_KEY] SUB_INDEX_KEY = "sub_index"
else: PATTERN_ID_KEY = "pattern_id"
self.__trackTags = {} EXTERNAL_SOURCE_FILE_PATH_KEY = "external_source_file"
if TrackDescriptor.DISPOSITION_SET_KEY in kwargs.keys(): DISPOSITION_SET_KEY = "disposition_set"
if type(kwargs[TrackDescriptor.DISPOSITION_SET_KEY]) is not set: TAGS_KEY = "tags"
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(): TRACK_TYPE_KEY = "track_type"
if type(kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY]) is not AudioLayout: AUDIO_LAYOUT_KEY = "audio_layout"
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
FFPROBE_INDEX_KEY = "index"
FFPROBE_DISPOSITION_KEY = "disposition"
FFPROBE_TAGS_KEY = "tags"
FFPROBE_CODEC_TYPE_KEY = "codec_type"
@classmethod def __init__(self, **kwargs):
def fromFfprobe(cls, streamObj):
"""Processes ffprobe stream data as array with elements according to the following example if TrackDescriptor.ID_KEY in kwargs.keys():
if type(kwargs[TrackDescriptor.ID_KEY]) is not int:
raise TypeError(
f"TrackDesciptor.__init__(): Argument {TrackDescriptor.ID_KEY} is required to be of type int"
)
self.__trackId = kwargs[TrackDescriptor.ID_KEY]
else:
self.__trackId = -1
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.EXTERNAL_SOURCE_FILE_PATH_KEY in kwargs.keys():
if type(kwargs[TrackDescriptor.EXTERNAL_SOURCE_FILE_PATH_KEY]) is not str:
raise TypeError(
f"TrackDesciptor.__init__(): Argument {TrackDescriptor.EXTERNAL_SOURCE_FILE_PATH_KEY} is required to be of type str"
)
self.__externalSourceFilePath = kwargs[TrackDescriptor.EXTERNAL_SOURCE_FILE_PATH_KEY]
else:
self.__externalSourceFilePath = None
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.SOURCE_INDEX_KEY in kwargs.keys()
and type(kwargs[TrackDescriptor.SOURCE_INDEX_KEY]) is int
):
self.__sourceIndex = kwargs[TrackDescriptor.SOURCE_INDEX_KEY]
else:
self.__sourceIndex = self.__index
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 int"
)
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, subIndex: int = -1):
"""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.INDEX_KEY] = (
int(streamObj[TrackDescriptor.FFPROBE_INDEX_KEY])
if TrackDescriptor.FFPROBE_INDEX_KEY in streamObj.keys()
else -1
)
kwargs[TrackDescriptor.SOURCE_INDEX_KEY] = kwargs[TrackDescriptor.INDEX_KEY]
kwargs[TrackDescriptor.SUB_INDEX_KEY] = subIndex
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = trackType
kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = (
{ {
"index": 4, t
"codec_name": "hdmv_pgs_subtitle", for d in (
"codec_long_name": "HDMV Presentation Graphic Stream subtitles", k
"codec_type": "subtitle", for (k, v) in streamObj[
"codec_tag_string": "[0][0][0][0]", TrackDescriptor.FFPROBE_DISPOSITION_KEY
"codec_tag": "0x0000", ].items()
"r_frame_rate": "0/0", if v
"avg_frame_rate": "0/0", )
"time_base": "1/1000", if (t := TrackDisposition.find(d)) is not None
"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.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
else AudioLayout.LAYOUT_UNDEFINED
)
trackType = TrackType.fromLabel(streamObj['codec_type']) if 'codec_type' in streamObj.keys() else TrackType.UNKNOWN return cls(**kwargs)
else:
return None
if trackType != TrackType.UNKNOWN: def getId(self):
return self.__trackId
kwargs = {} def getPatternId(self):
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = trackType return self.__patternId
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) def getIndex(self):
else: return self.__index
return None
def getSourceIndex(self):
return self.__sourceIndex
def getPatternId(self): def getSubIndex(self):
return self.__patternId return self.__subIndex
def getIndex(self): def setSubIndex(self, subIndex):
return self.__index self.__subIndex = subIndex
def getSubIndex(self): def getType(self):
return self.__subIndex return self.__trackType
def getLanguage(self):
if "language" in self.__trackTags.keys():
return IsoLanguage.findThreeLetter(self.__trackTags["language"])
else:
return IsoLanguage.UNDEFINED
def getType(self): def getTitle(self):
return self.__trackType if "title" in self.__trackTags.keys():
return str(self.__trackTags["title"])
else:
return ""
def getLanguage(self): def getAudioLayout(self):
if 'language' in self.__trackTags.keys(): return self.__audioLayout
return IsoLanguage.findThreeLetter(self.__trackTags['language'])
else:
return IsoLanguage.UNDEFINED
def getTitle(self): def getTags(self):
if 'title' in self.__trackTags.keys(): return self.__trackTags
return str(self.__trackTags['title'])
else:
return ''
def getAudioLayout(self): def getDispositionSet(self):
return self.__audioLayout return self.__dispositionSet
def getDispositionFlag(self, disposition: TrackDisposition) -> bool:
return bool(disposition in self.__dispositionSet)
def getTags(self): def setDispositionFlag(self, disposition: TrackDisposition, state: bool):
return self.__trackTags if state:
self.__dispositionSet.add(disposition)
def getDispositionSet(self): else:
return self.__dispositionSet self.__dispositionSet.discard(disposition)
def compare(self, vsTrackDescriptor):
compareResult = {}
tagsDiffResult = dictDiff(vsTrackDescriptor.getTags(), self.getTags())
if tagsDiffResult:
compareResult[TrackDescriptor.TAGS_KEY] = tagsDiffResult
vsDispositions = vsTrackDescriptor.getDispositionSet()
dispositions = self.getDispositionSet()
dispositionDiffResult = setDiff(vsDispositions, dispositions)
if dispositionDiffResult:
compareResult[TrackDescriptor.DISPOSITION_SET_KEY] = dispositionDiffResult
return compareResult
def setExternalSourceFilePath(self, filePath: str):
self.__externalSourceFilePath = str(filePath)
def getExternalSourceFilePath(self):
return self.__externalSourceFilePath

View File

@@ -3,16 +3,15 @@ import click, time
from textual import events from textual import events
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import Header, Footer, Placeholder, Label, ListView, ListItem, Static, DataTable, Button, Input, Checkbox, SelectionList, Select from textual.widgets import Header, Footer, Static, Button, SelectionList, Select, DataTable, Input
from textual.containers import Grid, Horizontal from textual.containers import Grid
from ffx.model.show import Show
from ffx.model.pattern import Pattern from ffx.model.pattern import Pattern
from ffx.model.track import Track
from .track_controller import TrackController from .track_controller import TrackController
from .pattern_controller import PatternController from .pattern_controller import PatternController
# from .show_controller import ShowController from .tag_controller import TagController
from .track_type import TrackType from .track_type import TrackType
@@ -22,6 +21,11 @@ from .audio_layout import AudioLayout
from .track_descriptor import TrackDescriptor from .track_descriptor import TrackDescriptor
from .tag_details_screen import TagDetailsScreen
from .tag_delete_screen import TagDeleteScreen
from textual.widgets._data_table import CellDoesNotExist
# Screen[dict[int, str, int]] # Screen[dict[int, str, int]]
class TrackDetailsScreen(Screen): class TrackDetailsScreen(Screen):
@@ -29,9 +33,9 @@ class TrackDetailsScreen(Screen):
CSS = """ CSS = """
Grid { Grid {
grid-size: 5 20; grid-size: 5 24;
grid-rows: 2 2 2 2 2 3 2 2 2 2 2 6 2 2 6 2 2 2 2 6; grid-rows: 2 2 2 2 2 3 3 2 2 3 2 2 2 2 2 6 2 2 6 2 2 2;
grid-columns: 25 25 25 25 225; grid-columns: 25 25 25 25 125;
height: 100%; height: 100%;
width: 100%; width: 100%;
padding: 1; padding: 1;
@@ -76,9 +80,13 @@ class TrackDetailsScreen(Screen):
height: 100%; height: 100%;
border: solid green; border: solid green;
} }
.yellow {
tint: yellow 40%;
}
""" """
def __init__(self, trackDescriptor = None, patternId = None, trackType : TrackType = None, subIndex = None): def __init__(self, trackDescriptor : TrackDescriptor = None, patternId = None, trackType : TrackType = None, index = None, subIndex = None):
super().__init__() super().__init__()
self.context = self.app.getContext() self.context = self.app.getContext()
@@ -86,47 +94,55 @@ class TrackDetailsScreen(Screen):
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.__tac = TagController(context = self.context)
INDEX_KEY = 'index'
SUB_INDEX_KEY = 'sub_index'
PATTERN_ID_KEY = 'pattern_id'
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 self.__isNew = trackDescriptor is None
if self.__isNew: if self.__isNew:
self.__trackType = trackType self.__trackType = trackType
self.__audioLayout = AudioLayout.LAYOUT_UNDEFINED
self.__index = index
self.__subIndex = subIndex self.__subIndex = subIndex
self.__trackDescriptor = None self.__trackDescriptor : TrackDescriptor = None
self.__pattern = self.__pc.getPattern(patternId) if patternId is not None else {} self.__pattern : Pattern = self.__pc.getPattern(patternId) if patternId is not None else {}
else: else:
self.__trackType = trackDescriptor.getType() self.__trackType = trackDescriptor.getType()
self.__audioLayout = trackDescriptor.getAudioLayout()
self.__index = trackDescriptor.getIndex()
self.__subIndex = trackDescriptor.getSubIndex() self.__subIndex = trackDescriptor.getSubIndex()
self.__trackDescriptor = trackDescriptor self.__trackDescriptor : TrackDescriptor = trackDescriptor
self.__pattern = self.__pc.getPattern(self.__trackDescriptor.getPatternId()) self.__pattern : Pattern = self.__pc.getPattern(self.__trackDescriptor.getPatternId())
def updateTags(self):
self.trackTagsTable.clear()
trackId = self.__trackDescriptor.getId()
if trackId != -1:
trackTags = self.__tac.findAllTrackTags(trackId)
for k,v in trackTags.items():
if k != 'language' and k != 'title':
row = (k,v)
self.trackTagsTable.add_row(*map(str, row))
def on_mount(self): def on_mount(self):
self.query_one("#index_label", Static).update(str(self.__index) if self.__index is not None else '-')
self.query_one("#subindex_label", Static).update(str(self.__subIndex)if self.__subIndex is not None else '-')
if self.__pattern is not None: if self.__pattern is not None:
self.query_one("#patternlabel", Static).update(self.__pattern.getPattern()) self.query_one("#pattern_label", Static).update(self.__pattern.getPattern())
if self.__subIndex is not None:
self.query_one("#subindexlabel", Static).update(str(self.__subIndex))
if self.__trackType is not None:
self.query_one("#type_select", Select).value = self.__trackType.label()
if self.__trackType == TrackType.AUDIO:
self.query_one("#audio_layout_select", Select).value = self.__audioLayout.label()
for d in TrackDisposition: for d in TrackDisposition:
@@ -140,6 +156,7 @@ class TrackDetailsScreen(Screen):
self.query_one("#language_select", Select).value = self.__trackDescriptor.getLanguage().label() self.query_one("#language_select", Select).value = self.__trackDescriptor.getLanguage().label()
self.query_one("#title_input", Input).value = self.__trackDescriptor.getTitle() self.query_one("#title_input", Input).value = self.__trackDescriptor.getTitle()
self.updateTags()
def compose(self): def compose(self):
@@ -148,7 +165,7 @@ class TrackDetailsScreen(Screen):
# Define the columns with headers # Define the columns with headers
self.column_key_track_tag_key = self.trackTagsTable.add_column("Key", width=10) self.column_key_track_tag_key = self.trackTagsTable.add_column("Key", width=10)
self.column_key_track_tag_value = self.trackTagsTable.add_column("Value", width=30) self.column_key_track_tag_value = self.trackTagsTable.add_column("Value", width=125)
self.trackTagsTable.cursor_type = 'row' self.trackTagsTable.cursor_type = 'row'
@@ -160,70 +177,87 @@ class TrackDetailsScreen(Screen):
with Grid(): with Grid():
# 1 # 1
yield Static(f"New {self.__trackType.label()} stream" if self.__isNew else f"Edit {self.__trackType.label()} stream", id="toplabel", classes="five") yield Static(f"New stream" if self.__isNew else f"Edit stream", id="toplabel", classes="five")
# 2 # 2
yield Static("for pattern") yield Static("for pattern")
yield Static("", id="patternlabel", classes="four") yield Static("", id="pattern_label", classes="four")
# 3 # 3
yield Static("sub index") yield Static(" ", classes="five")
yield Static("", id="subindexlabel", classes="four")
# 4 # 4
yield Static(" ", classes="five") yield Static("Index / Subindex")
yield Static("", id="index_label", classes="two")
yield Static("", id="subindex_label", classes="two")
# 5 # 5
yield Static(" ", classes="five") yield Static(" ", classes="five")
# 6 # 6
yield Static("Language") yield Static("Type")
yield Select.from_values(languages, classes="four", id="language_select") yield Select.from_values([t.label() for t in TrackType], classes="four", id="type_select")
# 7 # 7
yield Static(" ", classes="five") yield Static("Audio Layout")
yield Select.from_values([t.label() for t in AudioLayout], classes="four", id="audio_layout_select")
# 8 # 8
yield Static("Title") yield Static(" ", classes="five")
yield Input(id="title_input", classes="four")
# 9 # 9
yield Static(" ", classes="five") yield Static(" ", classes="five")
# 10 # 10
yield Static("Language")
yield Select.from_values(languages, classes="four", id="language_select")
# 11
yield Static(" ", classes="five") yield Static(" ", classes="five")
# 11
yield Static("Stream tags")
yield Static(" ", classes="two")
yield Button("Add", id="button_add_stream_tag")
yield Button("Delete", id="button_delete_stream_tag")
# 12 # 12
yield self.trackTagsTable yield Static("Title")
yield Input(id="title_input", classes="four")
# 13 # 13
yield Static(" ", classes="five") yield Static(" ", classes="five")
# 14 # 14
yield Static("Stream dispositions", classes="five") yield Static(" ", classes="five")
# 15 # 15
yield Static("Stream tags")
yield Static(" ")
yield Button("Add", id="button_add_stream_tag")
yield Button("Edit", id="button_edit_stream_tag")
yield Button("Delete", id="button_delete_stream_tag")
# 16
yield self.trackTagsTable
# 17
yield Static(" ", classes="five")
# 18
yield Static("Stream dispositions", classes="five")
# 19
yield SelectionList[int]( yield SelectionList[int](
classes="five", classes="five",
id = "dispositions_selection_list" id = "dispositions_selection_list"
) )
# 16 # 20
yield Static(" ", classes="five") yield Static(" ", classes="five")
# 17 # 21
yield Static(" ", classes="five") yield Static(" ", classes="five")
# 18 # 22
yield Button("Save", id="save_button") yield Button("Save", id="save_button")
yield Button("Cancel", id="cancel_button") yield Button("Cancel", id="cancel_button")
# 19 # 23
yield Static(" ", classes="five") yield Static(" ", classes="five")
# 20 # 24
yield Static(" ", classes="five", id="messagestatic") yield Static(" ", classes="five", id="messagestatic")
@@ -236,39 +270,65 @@ class TrackDetailsScreen(Screen):
kwargs[TrackDescriptor.PATTERN_ID_KEY] = int(self.__pattern.getId()) kwargs[TrackDescriptor.PATTERN_ID_KEY] = int(self.__pattern.getId())
kwargs[TrackDescriptor.INDEX_KEY] = -1 kwargs[TrackDescriptor.INDEX_KEY] = self.__index
kwargs[TrackDescriptor.SUB_INDEX_KEY] = self.__subIndex kwargs[TrackDescriptor.SUB_INDEX_KEY] = self.__subIndex #!
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = self.__trackType kwargs[TrackDescriptor.TRACK_TYPE_KEY] = TrackType.fromLabel(self.query_one("#type_select", Select).value)
kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = AudioLayout.fromLabel(self.query_one("#audio_layout_select", Select).value)
trackTags = {} trackTags = {}
language = self.query_one("#language_select", Select).value language = self.query_one("#language_select", Select).value
# raise click.ClickException(f"language={language}")
if language: if language:
trackTags['language'] = IsoLanguage.find(language).threeLetter() trackTags['language'] = IsoLanguage.find(language).threeLetter()
title = self.query_one("#title_input", Input).value title = self.query_one("#title_input", Input).value
if title: if title:
trackTags['title'] = title trackTags['title'] = title
kwargs[TrackDescriptor.TAGS_KEY] = trackTags
tableTags = {row[0]:row[1] for r in self.trackTagsTable.rows if (row := self.trackTagsTable.get_row(r)) and row[0] != 'language' and row[0] != 'title'}
kwargs[TrackDescriptor.TAGS_KEY] = trackTags | tableTags
dispositionFlags = 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) kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = TrackDisposition.toSet(dispositionFlags)
kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = AudioLayout.LAYOUT_UNDEFINED
return TrackDescriptor(**kwargs) return TrackDescriptor(**kwargs)
def getSelectedTag(self):
try:
# Fetch the currently selected row when 'Enter' is pressed
#selected_row_index = self.table.cursor_row
row_key, col_key = self.trackTagsTable.coordinate_to_cell_key(self.trackTagsTable.cursor_coordinate)
if row_key is not None:
selected_tag_data = self.trackTagsTable.get_row(row_key)
tagKey = str(selected_tag_data[0])
tagValue = str(selected_tag_data[1])
return tagKey, tagValue
else:
return None
except CellDoesNotExist:
return None
# 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()
# Check for multiple default/forced disposition flags # Check for multiple default/forced disposition flags
if self.__trackType == TrackType.VIDEO:
trackList = self.__tc.findVideoTracks(self.__pattern.getId())
if self.__trackType == TrackType.AUDIO: if self.__trackType == TrackType.AUDIO:
trackList = self.__tc.findAudioTracks(self.__pattern.getId()) trackList = self.__tc.findAudioTracks(self.__pattern.getId())
elif self.__trackType == TrackType.SUBTITLE: elif self.__trackType == TrackType.SUBTITLE:
@@ -276,35 +336,76 @@ class TrackDetailsScreen(Screen):
else: else:
trackList = [] trackList = []
siblingTrackList = [t for t in trackList if t.getType() == self.__trackType and t.getSubIndex() != self.__subIndex] siblingTrackList = [t for t in trackList if t.getType() == self.__trackType and t.getIndex() != self.__index]
numDefaultTracks = len([t for t in siblingTrackList if TrackDisposition.DEFAULT in t.getDispositionSet()]) 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()]) numForcedTracks = len([t for t in siblingTrackList if TrackDisposition.FORCED in t.getDispositionSet()])
self.__subIndex = len(trackList)
trackDescriptor = self.getTrackDescriptorFromInput()
if ((TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() and numDefaultTracks) if ((TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() and numDefaultTracks)
or (TrackDisposition.FORCED in trackDescriptor.getDispositionSet() and numForcedTracks)): 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") self.query_one("#messagestatic", Static).update("Cannot add another stream with disposition flag 'debug' or 'forced' set")
else: else:
self.query_one("#messagestatic", Static).update(" ") self.query_one("#messagestatic", Static).update(" ")
if self.__isNew: if self.__isNew:
# Track per Screen hinzufügen
self.__tc.addTrack(trackDescriptor) self.__tc.addTrack(trackDescriptor)
self.dismiss(trackDescriptor) self.dismiss(trackDescriptor)
else: else:
trackId = self.__tc.findTrack(self.__pattern.getId(), self.__trackType, self.__subIndex) track = self.__tc.getTrack(self.__pattern.getId(), self.__index)
if self.__tc.updateTrack(trackId, trackDescriptor): # Track per details screen updaten
if self.__tc.updateTrack(track.getId(), trackDescriptor):
self.dismiss(trackDescriptor) self.dismiss(trackDescriptor)
else: else:
self.app.pop_screen() self.app.pop_screen()
if event.button.id == "cancel_button": if event.button.id == "cancel_button":
self.app.pop_screen() self.app.pop_screen()
if event.button.id == "button_add_stream_tag":
if not self.__isNew:
self.app.push_screen(TagDetailsScreen(), self.handle_update_tag)
if event.button.id == "button_edit_stream_tag":
tagKey, tagValue = self.getSelectedTag()
self.app.push_screen(TagDetailsScreen(key=tagKey, value=tagValue), self.handle_update_tag)
if event.button.id == "button_delete_stream_tag":
tagKey, tagValue = self.getSelectedTag()
self.app.push_screen(TagDeleteScreen(key=tagKey, value=tagValue), self.handle_delete_tag)
def handle_update_tag(self, tag):
trackId = self.__trackDescriptor.getId()
if trackId == -1:
raise click.ClickException(f"TrackDetailsScreen.handle_update_tag: trackId not set (-1) trackDescriptor={self.__trackDescriptor}")
if self.__tac.updateTrackTag(trackId, tag[0], tag[1]) is not None:
self.updateTags()
def handle_delete_tag(self, trackTag):
trackId = self.__trackDescriptor.getId()
if trackId == -1:
raise click.ClickException(f"TrackDetailsScreen.handle_delete_tag: trackId not set (-1) trackDescriptor={self.__trackDescriptor}")
tag = self.__tac.findTrackTag(trackId, trackTag[0])
if tag is not None:
if self.__tac.deleteTrackTag(tag.id):
self.updateTags()

View File

@@ -13,21 +13,25 @@ class TrackType(Enum):
"""Returns the stream type as string""" """Returns the stream type as string"""
return str(self.value['label']) return str(self.value['label'])
def indicator(self):
"""Returns the stream type as single letter"""
return self.label()[0]
def index(self): def index(self):
"""Returns the stream type index""" """Returns the stream type index"""
return int(self.value['index']) return int(self.value['index'])
@staticmethod @staticmethod
def fromLabel(label): def fromLabel(label : str):
tlist = [t for t in TrackType if t.value['label'] == label] tlist = [t for t in TrackType if t.value['label'] == str(label)]
if tlist: if tlist:
return tlist[0] return tlist[0]
else: else:
return TrackType.UNKNOWN return TrackType.UNKNOWN
@staticmethod @staticmethod
def fromIndex(index): def fromIndex(index : int):
tlist = [t for t in TrackType if t.value['index'] == index] tlist = [t for t in TrackType if t.value['index'] == int(index)]
if tlist: if tlist:
return tlist[0] return tlist[0]
else: else:

32
bin/ffx/video_encoder.py Normal file
View File

@@ -0,0 +1,32 @@
from enum import Enum
class VideoEncoder(Enum):
AV1 = {'label': 'av1', 'index': 1}
VP9 = {'label': 'vp9', 'index': 2}
UNDEFINED = {'label': 'undefined', 'index': 0}
def label(self):
"""Returns the stream type as string"""
return str(self.value['label'])
def index(self):
"""Returns the stream type index"""
return int(self.value['index'])
@staticmethod
def fromLabel(label : str):
tlist = [t for t in VideoEncoder if t.value['label'] == str(label)]
if tlist:
return tlist[0]
else:
return VideoEncoder.UNDEFINED
@staticmethod
def fromIndex(index : int):
tlist = [t for t in VideoEncoder if t.value['index'] == int(index)]
if tlist:
return tlist[0]
else:
return VideoEncoder.UNDEFINED