55 Commits

Author SHA1 Message Date
Maveno
24d0700db2 nightly subtitle file import 2024-10-20 19:14:25 +02:00
Maveno
3463c1e371 improving unmux 2024-10-20 18:29:49 +02:00
Maveno
5ca7d6d12c unmux mwe 2024-10-20 17:58:07 +02:00
Maveno
77dfb4b1d3 reworking target media decriptor track handling 2024-10-20 16:20:33 +02:00
1df2e74566 ff 2024-10-19 20:26:14 +02:00
8f9f77e891 fixes ffmpeg parameter processing 2024-10-19 13:30:30 +02:00
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
30 changed files with 3113 additions and 1441 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
__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))

1058
bin/ffx.py

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,46 @@
import click
from enum import Enum
from .track_type import TrackType
class AudioLayout(Enum):
LAYOUT_STEREO = {"layout": "stereo", "index": 1}
LAYOUT_5_1 = {"layout": "5.1(side)", "index": 2}
LAYOUT_6_1 = {"layout": "6.1", "index": 3}
LAYOUT_7_1 = {"layout": "7.1", "index": 4} #TODO: Does this exist?
LAYOUT_STEREO = {"label": "stereo", "index": 1}
LAYOUT_5_1 = {"label": "5.1(side)", "index": 2}
LAYOUT_6_1 = {"label": "6.1", "index": 3}
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):
"""Returns the layout as string"""
return self.value['layout']
def label(self):
"""Returns the audio layout as string"""
return str(self.value['label'])
def index(self):
"""Returns the layout as string"""
return self.value['layout']
"""Returns the audio layout as integer"""
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_CHANNELS_KEY = 'channels'
@@ -34,7 +52,7 @@ class AudioLayout(Enum):
raise Exception('Not an ffprobe audio stream object')
if FFPROBE_LAYOUT_KEY in streamObj.keys():
matchingLayouts = [l for l in AudioLayout if l.value['layout'] == streamObj[FFPROBE_LAYOUT_KEY]]
matchingLayouts = [l for l in AudioLayout if l.label() == streamObj[FFPROBE_LAYOUT_KEY]]
if matchingLayouts:
return matchingLayouts[0]

View File

@@ -1,6 +1,7 @@
from textual.app import App
from .shows_screen import ShowsScreen
from .media_details_screen import MediaDetailsScreen
class FfxApp(App):
@@ -21,9 +22,17 @@ class FfxApp(App):
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):
"""Data 'output' method"""
return self.context

View File

@@ -1,2 +1,458 @@
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():
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):
outputFilePath = f"{filepath}.{ext}"
return ['-f', format, outputFilePath]
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]
targetAudioTrackDescriptors = [td for td in self.__targetMediaDescriptor.getAllTrackDescriptors() if td.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
# -disposition:s:0 default -disposition:s:1 0
def generateDispositionTokens(self):
# sourceTrackDescriptors = [] if self.__sourceMediaDescriptor is None else self.__sourceMediaDescriptor.getAllTrackDescriptors()
targetTrackDescriptors = self.__targetMediaDescriptor.getAllTrackDescriptors()
dispositionTokens = []
# raise click.ClickException(f"ttd subindices: {[t.getSubIndex() for t in targetTrackDescriptors]}")
#TODO: Sorting here is for the sole purpose to let the tokens appear with ascending subindices. Why necessary? Jellyfin order?
# for trackDescriptor in sorted(targetTrackDescriptors.copy(), key=lambda d: d.getSubIndex()):
for trackDescriptor in targetTrackDescriptors:
#HINT: No dispositions for pgs subtitle tracks that have no external file source
if (trackDescriptor.getExternalSourceFilePath()
or trackDescriptor.getCodec() != TrackDescriptor.CODEC_PGS):
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"""
#
# metadataTokens = []
#
# # 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()
#
# #TODO: Warum erscheint nur -1 im output?
# 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))
# pass
# #metadataTokens += [f"-map_metadata:g", "-1"]
#
# #for targetMediaTagKey in targetTags:
# #metadataTokens += [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
# metadataTokens += [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
# metadataTokens += [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]
#
# metadataTokens += [f"-map_metadata:s:{changedTargetTrackDescriptor.getType().indicator()}:{changedTargetSourceSubIndex}", "-1"]
#
# for targetTrackTagKey, targetTrackTagValue in changedTargetTrackDescriptor.getTags():
# metadataTokens += [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}")
#
# metadataTokens += [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}")
#
# metadataTokens += [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 metadataTokens
def generateMetadataTokens(self):
metadataTokens = []
for tagKey, tagValue in self.__targetMediaDescriptor.getTags().items():
metadataTokens += [f"-metadata:g",
f"{tagKey}={tagValue}"]
#HINT: With current ffmpeg version track metadata tags are not passed to the outfile
for td in self.__targetMediaDescriptor.getAllTrackDescriptors():
for tagKey, tagValue in td.getTags().items():
metadataTokens += [f"-metadata:s:{td.getType().indicator()}:{td.getSubIndex()}",
f"{tagKey}={tagValue}"]
return metadataTokens
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(only_video=True)
+ self.generateVP9Pass1Tokens(int(quality)))
if self.__context['perform_crop']:
commandSequence1 += self.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 += self.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']:
out, err, rc = executeProcess(commandSequence2)
if rc:
raise click.ClickException(f"Command resulted in error: rc={rc} error={err}")

View File

@@ -5,6 +5,10 @@ from .pattern_controller import PatternController
from .process import executeProcess
from ffx.model.pattern import Pattern
from ffx.ffx_controller import FfxController
from ffx.show_descriptor import ShowDescriptor
class FileProperties():
@@ -13,9 +17,12 @@ class FileProperties():
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)'
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
DEFAULT_INDEX_DIGITS = 3
def __init__(self, context, sourcePath):
self.context = context
# Separate basedir, basename and extension for current source file
self.__sourcePath = sourcePath
@@ -32,51 +39,26 @@ class FileProperties():
self.__sourceFilenameExtension = ''
se_match = re.compile(FileProperties.SEASON_EPISODE_INDICATOR_MATCH)
e_match = re.compile(FileProperties.EPISODE_INDICATOR_MATCH)
self.__pc = PatternController(context)
se_result = se_match.search(self.__sourceFilename)
e_result = e_match.search(self.__sourceFilename)
matchResult = self.__pc.matchFilename(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.__episode = -1
file_index = 0
if se_result is not None:
self.__season = int(se_result.group(1))
self.__episode = int(se_result.group(2))
elif e_result is not None:
self.__episode = int(e_result.group(1))
else:
file_index += 1
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]
#
if se_match is not None:
self.__season = int(se_match.group(1))
self.__episode = int(se_match.group(2))
elif e_match is not None:
self.__episode = int(e_match.group(1))
def getFormatData(self):
@@ -184,32 +166,73 @@ class FileProperties():
return json.loads(ffprobeOutput)['streams']
# def getTrackDescriptor(self, streamObj):
# """Convert the stream describing json object into a track descriptor"""
#
# trackType = streamObj['codec_type']
#
# descriptor = {}
#
# if trackType in [t.label() for t in TrackType]:
#
# descriptor['type'] = trackType
#
# descriptor = {}
# descriptor['disposition_list'] = [t for d in (k for (k,v) in streamObj['disposition'].items() if v) if (t := TrackDisposition.find(d)) if t is not None]
#
# descriptor['tags'] = streamObj['tags'] if 'tags' in streamObj.keys() else {}
#
# if trackType == TrackType.AUDIO.label():
# descriptor['layout'] = AudioLayout.identify(streamObj)
#
# return descriptor
def getMediaDescriptor(self):
return MediaDescriptor.fromFfprobe(self.getFormatData(), self.getStreamData())
# formatData = self.getFormatData()
# streamData = self.getStreamData()
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 getFilename(self):
return self.__sourceFilename
def getFileBasename(self):
return self.__sourceFileBasename
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

@@ -1,43 +1,429 @@
import os
import re
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 MediaDescriptor():
class MediaDescriptor:
"""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):
self.__mediaTags = kwargs['tags'] if 'tags' in kwargs.keys() else {}
self.__trackDescriptors = kwargs['trackDescriptors'] if 'trackDescriptors' in kwargs.keys() else {}
self.__clearTags = kwargs['clearTags'] if 'clearTags' in kwargs.keys() else False
if MediaDescriptor.TAGS_KEY in kwargs.keys():
if type(kwargs[MediaDescriptor.TAGS_KEY]) is not dict:
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.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 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 checkConfiguration(self):
videoTracks = self.getVideoTracks()
audioTracks = self.getAudioTracks()
subtitleTracks = self.getSubtitleTracks()
if len([v for v in videoTracks if v.getDispositionFlag(TrackDisposition.DEFAULT)]) > 1:
raise ValueError('More than one default video track')
if len([a for a in audioTracks if a.getDispositionFlag(TrackDisposition.DEFAULT)]) > 1:
raise ValueError('More than one default audio track')
if len([s for s in subtitleTracks if s.getDispositionFlag(TrackDisposition.DEFAULT)]) > 1:
raise ValueError('More than one default subtitle track')
if len([v for v in videoTracks if v.getDispositionFlag(TrackDisposition.FORCED)]) > 1:
raise ValueError('More than one forced video track')
if len([a for a in audioTracks if a.getDispositionFlag(TrackDisposition.FORCED)]) > 1:
raise ValueError('More than one forced audio track')
if len([s for s in subtitleTracks if s.getDispositionFlag(TrackDisposition.FORCED)]) > 1:
raise ValueError('More than one forced subtitle track')
trackDescriptors = videoTracks + audioTracks + subtitleTracks
sourceIndices = [
t.getSourceIndex() for t in trackDescriptors
]
if len(set(sourceIndices)) < len(trackDescriptors):
raise ValueError('Multiple streams originating from the same source stream')
def applyJellyfinOrder(self):
"""Reorder subtracks in types with default the last, then make subindices flat again"""
# videoTracks = self.sortSubIndices(self.getVideoTracks())
# audioTracks = self.sortSubIndices(self.getAudioTracks())
# subtitleTracks = self.sortSubIndices(self.getSubtitleTracks())
self.checkConfiguration()
# from self.__trackDescriptors
videoTracks = self.getVideoTracks()
audioTracks = self.getAudioTracks()
subtitleTracks = self.getSubtitleTracks()
defaultVideoTracks = [v for v in videoTracks if v.getDispositionFlag(TrackDisposition.DEFAULT)]
defaultAudioTracks = [a for a in audioTracks if a.getDispositionFlag(TrackDisposition.DEFAULT)]
defaultSubtitleTracks = [s for s in subtitleTracks if s.getDispositionFlag(TrackDisposition.DEFAULT)]
if defaultVideoTracks:
videoTracks.append(videoTracks.pop(videoTracks.index(defaultVideoTracks[0])))
self.sortSubIndices(videoTracks)
if defaultAudioTracks:
audioTracks.append(audioTracks.pop(audioTracks.index(defaultAudioTracks[0])))
self.sortSubIndices(audioTracks)
if defaultSubtitleTracks:
subtitleTracks.append(subtitleTracks.pop(subtitleTracks.index(defaultSubtitleTracks[0])))
self.sortSubIndices(subtitleTracks)
self.__trackDescriptors = videoTracks + audioTracks + subtitleTracks
self.sortIndices(self.__trackDescriptors)
@classmethod
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:
trackType = TrackType.fromLabel(streamObj['codec_type'])
ffprobeCodecType = streamObj[MediaDescriptor.FFPROBE_CODEC_TYPE_KEY]
trackType = TrackType.fromLabel(ffprobeCodecType)
if trackType != TrackType.UNKNOWN:
if trackType.label() not in descriptors.keys():
descriptors[trackType.label()] = []
if trackType not in subIndexCounters.keys():
subIndexCounters[trackType] = 0
descriptors[trackType.label()].append(TrackDescriptor.fromFfprobe(streamObj))
return cls(tags=formatData['tags'] if 'tags' in formatData.keys() else {},
trackDescriptors = descriptors)
kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY].append(
TrackDescriptor.fromFfprobe(
streamObj, subIndex=subIndexCounters[trackType]
)
)
subIndexCounters[trackType] += 1
return cls(**kwargs)
def getTags(self):
return self.__mediaTags
def getAudioTracks(self):
return self.__trackDescriptors[TrackType.AUDIO.label()] if TrackType.AUDIO.label() in self.__trackDescriptors.keys() else []
def sortSubIndices(
self, descriptors: List[TrackDescriptor]
) -> List[TrackDescriptor]:
subIndex = 0
for d in descriptors:
d.setSubIndex(subIndex)
subIndex += 1
return descriptors
def getSubtitleTracks(self):
return self.__trackDescriptors[TrackType.SUBTITLE.label()] if TrackType.SUBTITLE.label() in self.__trackDescriptors.keys() else []
def sortIndices(
self, descriptors: List[TrackDescriptor]
) -> List[TrackDescriptor]:
index = 0
for d in descriptors:
d.setIndex(index)
index += 1
return descriptors
def getAllTrackDescriptors(self) -> List[TrackDescriptor]:
return self.getVideoTracks() + self.getAudioTracks() + self.getSubtitleTracks()
def getVideoTracks(self) -> List[TrackDescriptor]:
return [
v for v in self.__trackDescriptors if v.getType() == TrackType.VIDEO
]
def getAudioTracks(self) -> List[TrackDescriptor]:
return [
a for a in self.__trackDescriptors if a.getType() == TrackType.AUDIO
]
def getSubtitleTracks(self) -> List[TrackDescriptor]:
return [
s
for s in self.__trackDescriptors
if s.getType() == TrackType.SUBTITLE
]
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()
# tags ist leer
# click.echo(f"tags={tags} vsTags={vsTags}")
# raise click.Abort
# 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.getAllTrackDescriptors() # filtern
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:
for td in self.__trackDescriptors:
importedFilePath = td.getExternalSourceFilePath()
if importedFilePath:
importFileTokens += [
"-i",
importedFilePath,
]
return importFileTokens
def getInputMappingTokens(self, use_sub_index: bool = True, only_video: bool = False):
# reorderedTrackDescriptors = self.getReorderedTrackDescriptors()
inputMappingTokens = []
filePointer = 1
#for rtd in reorderedTrackDescriptors:
for td in self.__trackDescriptors:
trackType = td.getType()
if (trackType == TrackType.VIDEO or not only_video):
importedFilePath = td.getExternalSourceFilePath()
if use_sub_index:
if importedFilePath:
inputMappingTokens += [
"-map",
f"{filePointer}:{trackType.indicator()}:0",
]
filePointer += 1
else:
if td.getCodec() != TrackDescriptor.CODEC_PGS:
inputMappingTokens += [
"-map",
f"0:{trackType.indicator()}:{td.getSubIndex()}",
]
else:
if td.getCodec() != TrackDescriptor.CODEC_PGS:
inputMappingTokens += ["-map", f"0:{td.getIndex()}"]
return inputMappingTokens
def searchSubtitleFiles(self, searchDirectory, prefix):
sesl_match = re.compile(MediaDescriptor.SEASON_EPISODE_STREAM_LANGUAGE_MATCH)
subtitleFileDescriptors = []
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["index"] = int(sesl_result.group(3))
subtitleFileDescriptor["language"] = sesl_result.group(4)
subtitleFileDescriptors.append(subtitleFileDescriptor)
click.echo(f"Available subtitle files {subtitleFileDescriptors}\n")
return subtitleFileDescriptors
def importSubtitles(self, searchDirectory, prefix, season: int = -1, episode: int = -1):
click.echo(f"Season: {season} Episode: {episode}")
availableFileSubtitleDescriptors = self.searchSubtitleFiles(searchDirectory, prefix)
click.echo(f"availableFileSubtitleDescriptors: {availableFileSubtitleDescriptors}")
subtitleTracks = self.getSubtitleTracks()
click.echo(f"subtitleTracks: {[s.getIndex() for s in subtitleTracks]}")
# if len(availableFileSubtitleDescriptors) != len(subtitleTracks):
# raise click.ClickException(f"MediaDescriptor.importSubtitles(): Number if subtitle files not matching number of subtitle tracks")
matchingSubtitleFileDescriptors = (
sorted(
[
d
for d in availableFileSubtitleDescriptors
if d["season"] == int(season) and d["episode"] == int(episode)
],
key=lambda d: d["index"],
)
if availableFileSubtitleDescriptors
else []
)
click.echo(f"matchingSubtitleFileDescriptors: {matchingSubtitleFileDescriptors}")
for msfd in matchingSubtitleFileDescriptors:
matchingSubtitleTrackDescriptor = [s for s in subtitleTracks if s.getIndex() == msfd["index"]]
if matchingSubtitleTrackDescriptor:
click.echo(f"Found matching subtitle file {msfd["path"]}\n")
matchingSubtitleTrackDescriptor[0].setExternalSourceFilePath(msfd["path"])

View File

@@ -0,0 +1,644 @@
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()
# keine tags vorhanden
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 tagKey, tagValue in self.__currentMediaDescriptor.getTags().items():
self.__tac.updateMediaTag(patternId, tagKey, tagValue)
for trackDescriptor in self.__currentMediaDescriptor.getAllTrackDescriptors():
self.__tc.addTrack(trackDescriptor, patternId = patternId)
def action_new_pattern(self):
try:
self.__currentMediaDescriptor.checkConfiguration()
except ValueError:
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.orm import relationship, sessionmaker, Mapped, backref
from .show import Base
from .show import Base, Show
from .track import Track
from ffx.media_descriptor import MediaDescriptor
from ffx.show_descriptor import ShowDescriptor
class Pattern(Base):
@@ -20,57 +23,52 @@ class Pattern(Base):
# v1.x
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
# show_id: Mapped[int] = mapped_column(ForeignKey("shows.id", ondelete="CASCADE"))
# 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):
#
# 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 getId(self):
return int(self.id)
def getShowId(self):
return int(self.show_id)
def getShow(self):
pass
def getTracks(self):
pass
def getMediaDescriptor(self):
md = MediaDescriptor(tags = self.getDescriptor()['tags'])
for t in self.tracks:
md.appendTrack(t.getDescriptor())
return md
def getShowDescriptor(self) -> ShowDescriptor:
click.echo(f"self.show {self.show} id={self.show_id}")
return self.show.getDescriptor()
def getId(self):
return int(self.id)
def getPattern(self):
return str(self.pattern)
def getShowId(self):
return int(self.show_id)
def getTags(self):
return {str(k.value):str(v.value) for (k,v) in self.media_tags}
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] = []
# Set ordered subindices
subIndexCounter = {}
for track in self.tracks:
trackType = track.getType()
if not trackType in subIndexCounter.keys():
subIndexCounter[trackType] = 0
kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY].append(track.getDescriptor(subIndex = subIndexCounter[trackType]))
subIndexCounter[trackType] += 1
return MediaDescriptor(**kwargs)

View File

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

View File

@@ -11,6 +11,8 @@ from ffx.iso_language import IsoLanguage
from ffx.track_disposition import TrackDisposition
from ffx.track_descriptor import TrackDescriptor
from ffx.audio_layout import AudioLayout
import click
class Track(Base):
@@ -32,21 +34,18 @@ class Track(Base):
track_type = Column(Integer) # TrackType
index = Column(Integer)
sub_index = Column(Integer)
source_index = Column(Integer)
# v1.x
pattern_id = Column(Integer, ForeignKey('patterns.id', ondelete="CASCADE"))
pattern = relationship('Pattern', back_populates='tracks')
# language = Column(String) # IsoLanguage threeLetter
# title = Column(String)
track_tags = relationship('TrackTag', back_populates='track', cascade="all, delete", lazy="joined")
disposition_flags = Column(Integer)
codec_name = Column(String)
audio_layout = Column(Integer)
def __init__(self, **kwargs):
@@ -55,10 +54,6 @@ class Track(Base):
if trackType is not None:
self.track_type = int(trackType)
# language = kwargs.pop('language', None)
# if language is not None:
# self.language = str(language.threeLetter())
dispositionSet = kwargs.pop(TrackDescriptor.DISPOSITION_SET_KEY, set())
self.disposition_flags = int(TrackDisposition.toFlags(dispositionSet))
@@ -66,7 +61,7 @@ class Track(Base):
@classmethod
def fromStreamObj(cls, streamObj, subIndex, patternId):
def fromFfprobeStreamObj(cls, streamObj, patternId):
"""{
'index': 4,
'codec_name': 'hdmv_pgs_subtitle',
@@ -133,14 +128,16 @@ class Track(Base):
"""
trackType = streamObj['codec_type']
trackType = streamObj[TrackDescriptor.FFPROBE_CODEC_TYPE_KEY]
if trackType in [t.label() for t in TrackType]:
return cls(pattern_id = patternId,
sub_index = int(subIndex),
track_type = trackType,
disposition_flags = sum([2**t.index() for (k,v) in streamObj['disposition'].items() if v and (t := TrackDisposition.find(k)) is not None]))
codec_name = streamObj[TrackDescriptor.FFPROBE_CODEC_NAME_KEY],
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:
return None
@@ -154,13 +151,16 @@ class Track(Base):
def getType(self):
return TrackType.fromIndex(self.track_type)
# def getIndex(self):
# return int(self.index)
def getSubIndex(self):
return int(self.sub_index)
def getCodec(self):
return str(self.codec_name)
def getIndex(self):
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):
tags = {t.key:t.value for t in self.track_tags}
return IsoLanguage.findThreeLetter(tags['language']) if 'language' in tags.keys() else IsoLanguage.UNDEFINED
@@ -172,22 +172,42 @@ class Track(Base):
def getDispositionSet(self):
return TrackDisposition.toSet(self.disposition_flags)
def getAudioLayout(self):
return AudioLayout.fromIndex(self.audio_layout)
def getTags(self):
return {str(t.key):str(t.value) for t in self.track_tags}
def getDescriptor(self) -> TrackDescriptor:
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.SUB_INDEX_KEY] = self.getIndex()
kwargs[TrackDescriptor.SUB_INDEX_KEY] = self.getSubIndex()
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.CODEC_NAME_KEY] = self.getCodec()
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

@@ -16,16 +16,16 @@ class PatternController():
try:
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():
pattern = Pattern(show_id = int(patternDescriptor['show_id']),
pattern = str(patternDescriptor['pattern']))
s.add(pattern)
s.commit()
return patternDescriptor
return pattern.getId()
else:
return {}
return None
except Exception as ex:
raise click.ClickException(f"PatternController.addPattern(): {repr(ex)}")
@@ -116,27 +116,27 @@ class PatternController():
s.close()
def matchFilename(self, filename):
def matchFilename(self, filename : str) -> re.Match:
try:
s = self.Session()
q = s.query(Pattern)
matchedPatterns = [p for p in q.all() if re.search(p.pattern, filename)]
if matchedPatterns:
return matchedPatterns[0]
else:
return None
matchResult = {}
for pattern in q.all():
patternMatch = re.search(str(pattern.pattern), str(filename))
if patternMatch:
matchResult['match'] = patternMatch
matchResult['pattern'] = pattern
return matchResult
except Exception as ex:
raise click.ClickException(f"PatternController.findPattern(): {repr(ex)}")
raise click.ClickException(f"PatternController.matchFilename(): {repr(ex)}")
finally:
s.close()
return result
def getMediaDescriptor(self, patternId):
try:
@@ -144,9 +144,9 @@ class PatternController():
q = s.query(Pattern).filter(Pattern.id == int(patternId))
if q.count():
pattern = q.first()
#return self.getPatternDict(pattern)
return pattern.getMediaDescriptor()
return q.first().getMediaDescriptor()
else:
return None
except Exception as ex:
raise click.ClickException(f"PatternController.getPatternDescriptor(): {repr(ex)}")

View File

@@ -53,7 +53,7 @@ class PatternDeleteScreen(Screen):
self.pattern_id = patternId
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):

View File

@@ -8,14 +8,19 @@ from textual.containers import Grid
from ffx.model.show import Show
from ffx.model.pattern import Pattern
from ffx.model.track import Track
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 .tag_details_screen import TagDetailsScreen
from .tag_delete_screen import TagDeleteScreen
from ffx.track_type import TrackType
from ffx.track_disposition import TrackDisposition
@@ -30,8 +35,8 @@ class PatternDetailsScreen(Screen):
CSS = """
Grid {
grid-size: 5 12;
grid-rows: 2 2 2 2 2 6 2 2 6 2 2 2;
grid-size: 5 13;
grid-rows: 2 2 2 2 2 8 2 2 8 2 2 2 2;
grid-columns: 25 25 25 25 25;
height: 100%;
width: 100%;
@@ -68,6 +73,10 @@ class PatternDetailsScreen(Screen):
height: 100%;
border: solid green;
}
.yellow {
tint: yellow 40%;
}
"""
def __init__(self, patternId = None, showId = None):
@@ -79,12 +88,13 @@ class PatternDetailsScreen(Screen):
self.__pc = PatternController(context = self.context)
self.__sc = ShowController(context = self.context)
self.__tc = TrackController(context = self.context)
self.__tac = TagController(context = self.context)
self.__pattern : Pattern = self.__pc.getPattern(patternId) if patternId is not None else None
self.show_obj = self.__sc.getShowDesciptor(showId) if showId is not None else {}
self.__showDescriptor = self.__sc.getShowDescriptor(showId) if showId is not None else None
#TODO: per controller
def loadTracks(self, show_id):
try:
@@ -104,88 +114,92 @@ class PatternDetailsScreen(Screen):
s.close()
def updateAudioTracks(self):
def updateTracks(self):
self.audioStreamsTable.clear()
self.tracksTable.clear()
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(),
" ",
at.getLanguage().label(),
at.getTitle(),
td : TrackDescriptor = tr.getDescriptor()
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.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:
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(),
" ",
st.getLanguage().label(),
st.getTitle(),
'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
self.subtitleStreamsTable.add_row(*map(str, row))
for tagKey, tagValue in tags.items():
row = (tagKey, tagValue)
self.tagsTable.add_row(*map(str, row))
def on_mount(self):
if self.show_obj:
self.query_one("#showlabel", Static).update(f"{self.show_obj['id']} - {self.show_obj['name']} ({self.show_obj['year']})")
if not self.__showDescriptor is None:
self.query_one("#showlabel", Static).update(f"{self.__showDescriptor.getId()} - {self.__showDescriptor.getName()} ({self.__showDescriptor.getYear()})")
if self.__pattern is not None:
self.query_one("#pattern_input", Input).value = str(self.__pattern.getPattern())
self.updateAudioTracks()
self.updateSubtitleTracks()
self.updateTags()
self.updateTracks()
def compose(self):
self.audioStreamsTable = DataTable(classes="five")
self.tagsTable = DataTable(classes="five")
# Define the columns with headers
self.column_key_audio_subid = self.audioStreamsTable.add_column("Subindex", width=20)
self.column_key_audio_layout = self.audioStreamsTable.add_column("Layout", width=20)
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.column_key_tag_key = self.tagsTable.add_column("Key", width=10)
self.column_key_tag_value = self.tagsTable.add_column("Value", width=100)
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_subtitle_subid = self.subtitleStreamsTable.add_column("Subindex", width=20)
self.column_key_subtitle_spacer = self.subtitleStreamsTable.add_column(" ", width=20)
self.column_key_subtitle_language = self.subtitleStreamsTable.add_column("Language", width=20)
self.column_key_subtitle_title = self.subtitleStreamsTable.add_column("Title", width=30)
self.column_key_subtitle_default = self.subtitleStreamsTable.add_column("Default", width=10)
self.column_key_subtitle_forced = self.subtitleStreamsTable.add_column("Forced", width=10)
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_audio_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.subtitleStreamsTable.cursor_type = 'row'
self.tracksTable.cursor_type = 'row'
yield Header()
@@ -199,7 +213,7 @@ class PatternDetailsScreen(Screen):
# 2
yield Static("from show")
yield Static("", id="showlabel", classes="three")
yield Button("Substitute pattern", id="patternbutton")
yield Button("Substitute pattern", id="pattern_button")
# 3
yield Static(" ", classes="five")
@@ -207,44 +221,51 @@ class PatternDetailsScreen(Screen):
yield Static(" ", classes="five")
# 5
yield Static("Audio streams")
yield Static("Media Tags")
yield Static(" ")
if self.__pattern is not None:
yield Button("Add", id="button_add_audio_stream")
yield Button("Edit", id="button_edit_audio_stream")
yield Button("Delete", id="button_delete_audio_stream")
yield Button("Add", id="button_add_tag")
yield Button("Edit", id="button_edit_tag")
yield Button("Delete", id="button_delete_tag")
else:
yield Static("")
yield Static("")
yield Static("")
yield Static(" ")
yield Static(" ")
yield Static(" ")
# 6
yield self.audioStreamsTable
yield self.tagsTable
# 7
yield Static(" ", classes="five")
# 8
yield Static("Subtitle streams")
yield Static("Streams")
yield Static(" ")
if self.__pattern is not None:
yield Button("Add", id="button_add_subtitle_stream")
yield Button("Edit", id="button_edit_subtitle_stream")
yield Button("Delete", id="button_delete_subtitle_stream")
yield Button("Add", id="button_add_track")
yield Button("Edit", id="button_edit_track")
yield Button("Delete", id="button_delete_track")
else:
yield Static("")
yield Static("")
yield Static("")
yield Static(" ")
yield Static(" ")
yield Static(" ")
# 9
yield self.subtitleStreamsTable
yield self.tracksTable
# 10
yield Static(" ", classes="five")
# 11
yield Static(" ", classes="five")
# 12
yield Button("Save", id="save_button")
yield Button("Cancel", id="cancel_button")
yield Static(" ", classes="three")
# 13
yield Static(" ", classes="five")
yield Footer()
@@ -254,7 +275,7 @@ class PatternDetailsScreen(Screen):
def getSelectedAudioTrackDescriptor(self):
def getSelectedTrackDescriptor(self):
if not self.__pattern:
return None
@@ -263,14 +284,15 @@ class PatternDetailsScreen(Screen):
# Fetch the currently selected row when 'Enter' is pressed
#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:
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])
return self.__tc.findTrack(self.__pattern.getId(), TrackType.AUDIO, subIndex).getDescriptor()
return self.__tc.getTrack(self.__pattern.getId(), trackIndex).getDescriptor(subIndex=trackSubIndex)
else:
return None
@@ -279,23 +301,22 @@ class PatternDetailsScreen(Screen):
return None
def getSelectedSubtitleTrackDescriptor(self) -> TrackDescriptor:
if not self.__pattern is None:
return None
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.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:
selected_tag_data = self.tagsTable.get_row(row_key)
selected_track_data = self.subtitleStreamsTable.get_row(row_key)
subIndex = int(selected_track_data[0])
tagKey = str(selected_tag_data[0])
tagValue = str(selected_tag_data[1])
return self.__tc.findTrack(self.__pattern.getId(), TrackType.SUBTITLE, subIndex).getDescriptor()
return tagKey, tagValue
else:
return None
@@ -311,7 +332,7 @@ class PatternDetailsScreen(Screen):
if event.button.id == "save_button":
patternDescriptor = {}
patternDescriptor['show_id'] = self.show_obj['id']
patternDescriptor['show_id'] = self.__showDescriptor.getId()
patternDescriptor['pattern'] = self.getPatternFromInput()
if self.__pattern is not None:
@@ -323,14 +344,15 @@ class PatternDetailsScreen(Screen):
self.app.pop_screen()
else:
if self.__pc.addPattern(patternDescriptor):
patternId = self.__pc.addPattern(patternDescriptor)
if patternId is not None:
self.dismiss(patternDescriptor)
else:
#TODO: Meldung
self.app.pop_screen()
if event.button.id == "cancel_button":
self.app.pop_screen()
@@ -338,28 +360,33 @@ class PatternDetailsScreen(Screen):
# Save pattern when just created before adding streams
if self.__pattern is not None:
if event.button.id == "button_add_audio_stream":
self.app.push_screen(TrackDetailsScreen(trackType = TrackType.AUDIO, patternId = self.__pattern.getId(), subIndex = len(self.audioStreamsTable.rows)), self.handle_add_track)
numTracks = len(self.tracksTable.rows)
selectedAudioTrack = self.getSelectedAudioTrackDescriptor()
if selectedAudioTrack is not None:
if event.button.id == "button_edit_audio_stream":
if event.button.id == "button_add_track":
self.app.push_screen(TrackDetailsScreen(patternId = self.__pattern.getId(), index = numTracks), self.handle_add_track)
self.app.push_screen(TrackDetailsScreen(trackDescriptor = selectedAudioTrack), self.handle_edit_track)
if event.button.id == "button_delete_audio_stream":
self.app.push_screen(TrackDeleteScreen(trackDescriptor = selectedAudioTrack), self.handle_delete_track)
if event.button.id == "button_add_subtitle_stream":
self.app.push_screen(TrackDetailsScreen(trackType = TrackType.SUBTITLE, patternId = self.__pattern.getId(), subIndex = len(self.subtitleStreamsTable.rows)), self.handle_add_track)
selectedTrack = self.getSelectedTrackDescriptor()
if selectedTrack is not None:
if event.button.id == "button_edit_track":
self.app.push_screen(TrackDetailsScreen(trackDescriptor = selectedTrack), self.handle_edit_track)
if event.button.id == "button_delete_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]+)'
@@ -371,70 +398,60 @@ class PatternDetailsScreen(Screen):
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()
trackType = trackDescriptor.getType()
index = trackDescriptor.getIndex()
subIndex = trackDescriptor.getSubIndex()
language = trackDescriptor.getLanguage()
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,
" ",
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))
self.tracksTable.add_row(*map(str, row))
def handle_edit_track(self, trackDescriptor : TrackDescriptor):
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.audioStreamsTable.update_cell(row_key, self.column_key_audio_title, trackDescriptor.getTitle())
self.audioStreamsTable.update_cell(row_key, self.column_key_audio_default, 'Yes' if TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() else 'No')
self.audioStreamsTable.update_cell(row_key, self.column_key_audio_forced, 'Yes' if TrackDisposition.FORCED in trackDescriptor.getDispositionSet() else 'No')
if trackDescriptor.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')
self.tracksTable.update_cell(row_key, self.column_key_track_audio_layout, trackDescriptor.getAudioLayout().label())
self.tracksTable.update_cell(row_key, self.column_key_track_language, trackDescriptor.getLanguage().label())
self.tracksTable.update_cell(row_key, self.column_key_track_title, trackDescriptor.getTitle())
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')
except CellDoesNotExist:
pass
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:
pass
def handle_update_tag(self, tag):
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,6 +1,9 @@
import subprocess
from typing import List
def executeProcess(commandSequence):
process = subprocess.Popen(commandSequence, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def executeProcess(commandSequence: List[str]):
# process = subprocess.Popen([t.encode('utf-8') for t in commandSequence], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process = subprocess.Popen(commandSequence, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8')
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
return output, error, process.returncode

View File

@@ -1,6 +1,7 @@
import click
from ffx.model.show import Show
from ffx.show_descriptor import ShowDescriptor
class ShowController():
@@ -11,36 +12,65 @@ class ShowController():
self.Session = self.context['database']['session'] # convenience
def getShowDesciptor(self, showId):
def getShowDescriptor(self, showId):
try:
s = self.Session()
q = s.query(Show).filter(Show.id == showId)
if q.count():
show = q.first()
return show.getDesciptor()
show: Show = q.first()
return show.getDescriptor()
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:
s.close()
def updateShow(self, showDescriptor):
def updateShow(self, showDescriptor: ShowDescriptor):
try:
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():
show = Show(id = int(showDescriptor['id']),
name = str(showDescriptor['name']),
year = int(showDescriptor['year']),
index_season_digits = showDescriptor['index_season_digits'],
index_episode_digits = showDescriptor['index_episode_digits'],
indicator_season_digits = showDescriptor['indicator_season_digits'],
indicator_episode_digits = showDescriptor['indicator_episode_digits'])
show = Show(id = int(showDescriptor.getId()),
name = str(showDescriptor.getName()),
year = int(showDescriptor.getYear()),
index_season_digits = showDescriptor.getIndexSeasonDigits(),
index_episode_digits = showDescriptor.getIndexEpisodeDigits(),
indicator_season_digits = showDescriptor.getIndicatorSeasonDigits(),
indicator_episode_digits = showDescriptor.getIndicatorEpisodeDigits())
s.add(show)
s.commit()
@@ -50,24 +80,24 @@ class ShowController():
currentShow = q.first()
changed = False
if currentShow.name != str(showDescriptor['name']):
currentShow.name = str(showDescriptor['name'])
if currentShow.name != str(showDescriptor.getName()):
currentShow.name = str(showDescriptor.getName())
changed = True
if currentShow.year != int(showDescriptor['year']):
currentShow.year = int(showDescriptor['year'])
if currentShow.year != int(showDescriptor.getYear()):
currentShow.year = int(showDescriptor.getYear())
changed = True
if currentShow.index_season_digits != int(showDescriptor['index_season_digits']):
currentShow.index_season_digits = int(showDescriptor['index_season_digits'])
if currentShow.index_season_digits != int(showDescriptor.getIndexSeasonDigits()):
currentShow.index_season_digits = int(showDescriptor.getIndexSeasonDigits())
changed = True
if currentShow.index_episode_digits != int(showDescriptor['index_episode_digits']):
currentShow.index_episode_digits = int(showDescriptor['index_episode_digits'])
if currentShow.index_episode_digits != int(showDescriptor.getIndexEpisodeDigits()):
currentShow.index_episode_digits = int(showDescriptor.getIndexEpisodeDigits())
changed = True
if currentShow.indicator_season_digits != int(showDescriptor['indicator_season_digits']):
currentShow.indicator_season_digits = int(showDescriptor['indicator_season_digits'])
if currentShow.indicator_season_digits != int(showDescriptor.getIndicatorSeasonDigits()):
currentShow.indicator_season_digits = int(showDescriptor.getIndicatorSeasonDigits())
changed = True
if currentShow.indicator_episode_digits != int(showDescriptor['indicator_episode_digits']):
currentShow.indicator_episode_digits = int(showDescriptor['indicator_episode_digits'])
if currentShow.indicator_episode_digits != int(showDescriptor.getIndicatorEpisodeDigits()):
currentShow.indicator_episode_digits = int(showDescriptor.getIndicatorEpisodeDigits())
changed = True
if changed:

View File

@@ -45,14 +45,13 @@ class ShowDeleteScreen(Screen):
self.Session = self.context['database']['session'] # convenience
self.__sc = ShowController(context = self.context)
self.show_obj = self.__sc.getShowDesciptor(showId) if showId is not None else {}
self.__showDescriptor = self.__sc.getShowDescriptor(showId) if showId is not None else {}
def on_mount(self):
if self.show_obj:
self.query_one("#showlabel", Static).update(f"{self.show_obj['id']} - {self.show_obj['name']} ({self.show_obj['year']})")
if not self.__showDescriptor is None:
self.query_one("#showlabel", Static).update(f"{self.__showDescriptor.getId()} - {self.__showDescriptor.getName()} ({self.__showDescriptor.getYear()})")
def compose(self):
@@ -84,8 +83,9 @@ class ShowDeleteScreen(Screen):
if event.button.id == "delete_button":
if self.__sc.deleteShow(self.show_obj['id']):
self.dismiss(self.show_obj['id'])
if not self.__showDescriptor is None:
if self.__sc.deleteShow(self.__showDescriptor.getId()):
self.dismiss(self.__showDescriptor)
else:
#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,4 +1,5 @@
import click
from datetime import datetime
from textual.screen import Screen
from textual.widgets import Header, Footer, Static, Button, DataTable, Input
@@ -14,6 +15,12 @@ from .pattern_delete_screen import PatternDeleteScreen
from .show_controller import ShowController
from .pattern_controller import PatternController
from .tmdb_controller import TmdbController
from .show_descriptor import ShowDescriptor
from .helper import filterFilename
# Screen[dict[int, str, int]]
class ShowDetailsScreen(Screen):
@@ -78,38 +85,43 @@ class ShowDetailsScreen(Screen):
self.__sc = ShowController(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:
s = self.Session()
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:
click.ClickException(f"loadPatterns(): {repr(ex)}")
raise click.ClickException(f"ShowDetailsScreen.loadPatterns(): {repr(ex)}")
finally:
s.close()
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("#name_input", Input).value = str(self.show_obj['name'])
self.query_one("#year_input", Input).value = str(self.show_obj['year'])
self.query_one("#id_static", Static).update(str(self.__showDescriptor.getId()))
self.query_one("#name_input", Input).value = str(self.__showDescriptor.getName())
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_episode_digits_input", Input).value = str(self.show_obj['index_episode_digits'])
self.query_one("#indicator_season_digits_input", Input).value = str(self.show_obj['indicator_season_digits'])
self.query_one("#indicator_episode_digits_input", Input).value = str(self.show_obj['indicator_episode_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.__showDescriptor.getIndexEpisodeDigits())
self.query_one("#indicator_season_digits_input", Input).value = str(self.__showDescriptor.getIndicatorSeasonDigits())
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'],)
self.patternTable.add_row(*map(str, row))
@@ -134,7 +146,7 @@ class ShowDetailsScreen(Screen):
if row_key is not None:
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])
except CellDoesNotExist:
@@ -147,8 +159,8 @@ class ShowDetailsScreen(Screen):
def action_add_pattern(self):
if self.show_obj:
self.app.push_screen(PatternDetailsScreen(showId = self.show_obj['id']), self.handle_add_pattern) # <-
if not self.__showDescriptor is None:
self.app.push_screen(PatternDetailsScreen(showId = self.__showDescriptor.getId()), self.handle_add_pattern) # <-
def handle_add_pattern(self, screenResult):
@@ -166,9 +178,9 @@ class ShowDetailsScreen(Screen):
selectedPatternId = self.__pc.findPattern(selectedPatternDescriptor)
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):
@@ -193,7 +205,7 @@ class ShowDetailsScreen(Screen):
if selectedPatternId is None:
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):
@@ -221,14 +233,16 @@ class ShowDetailsScreen(Screen):
with Grid():
# 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
yield Static("ID")
if self.show_obj:
yield Static("", id="id_wdg", classes="four")
if not self.__showDescriptor is None:
yield Static("", id="id_static", classes="four")
else:
yield Input(type="integer", id="id_wdg", classes="four")
yield Input(type="integer", id="id_input", classes="four")
# 3
yield Static("Name")
@@ -278,22 +292,44 @@ class ShowDetailsScreen(Screen):
def getShowDescriptorFromInput(self):
showDescriptor = {}
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)
kwargs = {}
showDescriptor['index_season_digits'] = int(self.query_one("#index_season_digits_input", Input).value)
showDescriptor['index_episode_digits'] = int(self.query_one("#index_episode_digits_input", Input).value)
showDescriptor['indicator_season_digits'] = int(self.query_one("#indicator_season_digits_input", Input).value)
showDescriptor['indicator_episode_digits'] = int(self.query_one("#indicator_episode_digits_input", Input).value)
try:
if self.__showDescriptor:
kwargs[ShowDescriptor.ID_KEY] = int(self.__showDescriptor.getId())
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
@@ -303,12 +339,23 @@ class ShowDetailsScreen(Screen):
showDescriptor = self.getShowDescriptorFromInput()
if self.__sc.updateShow(showDescriptor):
self.dismiss(showDescriptor)
else:
#TODO: Meldung
self.app.pop_screen()
if not showDescriptor is None:
if self.__sc.updateShow(showDescriptor):
self.dismiss(showDescriptor)
else:
#TODO: Meldung
self.app.pop_screen()
if event.button.id == "cancel_button":
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
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import Header, Footer, Placeholder, Label, ListView, ListItem, Static, DataTable, Button
from textual.containers import Grid
from ffx.model.show import Show
from .show_controller import ShowController
from .show_details_screen import ShowDetailsScreen
from .show_delete_screen import ShowDeleteScreen
from ffx.show_descriptor import ShowDescriptor
from .help_screen import HelpScreen
from textual.widgets._data_table import CellDoesNotExist
@@ -56,9 +59,9 @@ class ShowsScreen(Screen):
super().__init__()
self.context = self.app.getContext()
self.Session = self.context['database']['session'] # convenience
self.__sc = ShowController(context = self.context)
def getSelectedShowId(self):
@@ -96,14 +99,14 @@ class ShowsScreen(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:
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_year, screenResult['year'])
self.table.update_cell(row_key, self.column_key_name, showDescriptor.getName())
self.table.update_cell(row_key, self.column_key_year, showDescriptor.getYear())
except CellDoesNotExist:
pass
@@ -119,7 +122,7 @@ class ShowsScreen(Screen):
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:
row_key, col_key = self.table.coordinate_to_cell_key(self.table.cursor_coordinate)
@@ -129,24 +132,10 @@ class ShowsScreen(Screen):
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:
for show in self.loadShows():
self.table.add_row(*map(str, show)) # Convert each element to a string before adding
for show in self.__sc.getAllShows():
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):

View File

@@ -14,19 +14,18 @@ class TagController():
self.Session = self.context['database']['session'] # convenience
def updateMediaTag(self, trackId, tagKey, tagValue):
def updateMediaTag(self, patternId, tagKey, tagValue):
try:
s = self.Session()
q = s.query(MediaTag).filter(MediaTag.track_id == int(trackId),
MediaTag.key == str(tagKey),
MediaTag.value == str(tagValue))
q = s.query(MediaTag).filter(MediaTag.pattern_id == int(patternId),
MediaTag.key == str(tagKey))
tag = q.first()
if tag:
tag.value = str(tagValue)
else:
tag = MediaTag(track_id = int(trackId),
tag = MediaTag(pattern_id = int(patternId),
key = str(tagKey),
value = str(tagValue))
s.add(tag)
@@ -45,8 +44,7 @@ class TagController():
s = self.Session()
q = s.query(TrackTag).filter(TrackTag.track_id == int(trackId),
TrackTag.key == str(tagKey),
TrackTag.value == str(tagValue))
TrackTag.key == str(tagKey))
tag = q.first()
if tag:
tag.value = str(tagValue)
@@ -64,13 +62,52 @@ class TagController():
finally:
s.close()
def deleteMediaTagByKey(self, patternId, tagKey):
def findAllMediaTags(self, trackId) -> dict:
try:
s = self.Session()
q = s.query(MediaTag).filter(MediaTag.pattern_id == int(patternId),
MediaTag.key == str(tagKey))
if q.count():
tag = q.first()
s.delete(tag)
s.commit()
return True
else:
return False
except Exception as ex:
raise click.ClickException(f"TagController.deleteMediaTagByKey(): {repr(ex)}")
finally:
s.close()
def deleteTrackTagByKey(self, trackId, tagKey):
try:
s = self.Session()
q = s.query(TrackTag).filter(TrackTag.track_id == int(trackId),
TrackTag.key == str(tagKey))
tag = q.first()
if tag:
s.delete(tag)
s.commit()
return True
else:
return False
except Exception as ex:
raise click.ClickException(f"TagController.deleteTrackTagByKey(): {repr(ex)}")
finally:
s.close()
def findAllMediaTags(self, patternId) -> dict:
try:
s = self.Session()
q = s.query(MediaTag).filter(MediaTag.track_id == int(trackId))
q = s.query(MediaTag).filter(MediaTag.pattern_id == int(patternId))
if q.count():
return {t.key:t.value for t in q.all()}

View File

@@ -1,4 +1,4 @@
import os, click, requests
import os, click, requests, json
class TmdbController():
@@ -14,8 +14,65 @@ class TmdbController():
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:
#TODO: Content Type aware processing
# response = requests.get(tmdbUrl)
# response.encoding = 'utf-8'
# return response.json()
# response = requests.get(tmdbUrl)
# contentType = response.headers.get('Content-Type')
# print(content_type) # Example: 'application/json; charset=UTF-8'
# decoded_content = response.content.decode('utf-8')
# return json.loads(decoded_content)
return requests.get(tmdbUrl).json()
except:
return {}
def queryEpisode(self, showId, season, episode):
"""
First level keys in the response object:
air_date str 'YYY-MM-DD'
@@ -37,15 +94,17 @@ class TmdbController():
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 getEpisodeFilename(self,
def getEpisodeFileBasename(self,
showName,
episodeName,
season,
episode,
extension,
indexSeasonDigits = 2,
indexEpisodeDigits = 2,
indicatorSeasonDigits = 2,
@@ -92,7 +151,5 @@ class TmdbController():
filenameTokens += ['S{num:{fill}{width}}'.format(num=season, fill='0', width=indicatorSeasonDigits)]
if indicatorEpisodeDigits:
filenameTokens += ['E{num:{fill}{width}}'.format(num=episode, fill='0', width=indicatorEpisodeDigits)]
filenameTokens += ['.', extension]
return ''.join(filenameTokens)

View File

@@ -21,14 +21,20 @@ class TrackController():
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:
s = self.Session()
track = Track(pattern_id = int(trackDescriptor.getPatternId()),
track = Track(pattern_id = patId,
track_type = int(trackDescriptor.getType().index()),
sub_index = int(trackDescriptor.getSubIndex()),
disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet())))
codec_name = str(trackDescriptor.getCodec()),
index = int(trackDescriptor.getIndex()),
source_index = int(trackDescriptor.getSourceIndex()),
disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet())),
audio_layout = trackDescriptor.getAudioLayout().index())
s.add(track)
s.commit()
@@ -60,7 +66,10 @@ class TrackController():
track : Track = q.first()
track.sub_index = int(trackDescriptor.getSubIndex())
track.track_type = int(trackDescriptor.getType().index())
track.codec_name = str(trackDescriptor.getCodec())
track.audio_layout = int(trackDescriptor.getAudioLayout().index())
track.disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet()))
descriptorTagKeys = trackDescriptor.getTags()
@@ -88,6 +97,32 @@ class TrackController():
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:
s.close()
def findAudioTracks(self, patternId):
@@ -116,11 +151,11 @@ class TrackController():
s.close()
def findTrack(self, patternId : int, trackType : TrackType, subIndex : int) -> Track:
def getTrack(self, patternId : int, index: int) -> Track:
try:
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():
return q.first()
@@ -128,38 +163,78 @@ class TrackController():
return None
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:
s.close()
def deleteTrack(self, trackId):
try:
s = self.Session()
q = s.query(Track).filter(Track.id == int(trackId))
if q.count():
patternId = int(q.first().pattern_id)
track = q.first()
q_siblings = s.query(Track).filter(Track.pattern_id == patternId).order_by(Track.index)
q_siblings = s.query(Track).filter(Track.pattern_id == track.getPatternId(), Track.track_type == track.getType().index()).order_by(Track.sub_index)
subIndex = 0
index = 0
for track in q_siblings.all():
if track.sub_index == track.getSubIndex():
if track.id == int(trackId):
s.delete(track)
else:
track.sub_index = subIndex
subIndex += 1
track.index = index
index += 1
s.commit()
return True
return False
except Exception as ex:
raise click.ClickException(f"TrackController.deleteTrack(): {repr(ex)}")
finally:
s.close()
def setDefaultSubTrack(self, trackType, subIndex):
pass
def setForcedSubTrack(self, trackType, subIndex):
pass

View File

@@ -7,6 +7,7 @@ from textual.widgets import Header, Footer, Static, Button
from textual.containers import Grid
from ffx.model.pattern import Pattern
from ffx.model.track import Track
from ffx.track_descriptor import TrackDescriptor
from .track_controller import TrackController
@@ -121,7 +122,7 @@ class TrackDeleteScreen(Screen):
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:
raise click.ClickException(f"Track is none: patternId={self.__trackDescriptor.getPatternId()} type={self.__trackDescriptor.getType()} subIndex={self.__trackDescriptor.getSubIndex()}")

View File

@@ -1,181 +1,315 @@
import click
from .iso_language import IsoLanguage
from .track_type import TrackType
from .audio_layout import AudioLayout
from .track_disposition import TrackDisposition
from .helper import dictDiff, setDiff
class TrackDescriptor():
class TrackDescriptor:
ID_KEY = "id"
INDEX_KEY = "index"
SOURCE_INDEX_KEY = "source_index"
SUB_INDEX_KEY = "sub_index"
PATTERN_ID_KEY = "pattern_id"
EXTERNAL_SOURCE_FILE_PATH_KEY = "external_source_file"
DISPOSITION_SET_KEY = "disposition_set"
TAGS_KEY = "tags"
TRACK_TYPE_KEY = "track_type"
CODEC_NAME_KEY = "codec_name"
AUDIO_LAYOUT_KEY = "audio_layout"
FFPROBE_INDEX_KEY = "index"
FFPROBE_DISPOSITION_KEY = "disposition"
FFPROBE_TAGS_KEY = "tags"
FFPROBE_CODEC_TYPE_KEY = "codec_type"
FFPROBE_CODEC_NAME_KEY = "codec_name"
ID_KEY = 'id'
INDEX_KEY = 'index'
SUB_INDEX_KEY = 'sub_index'
PATTERN_ID_KEY = 'pattern_id'
CODEC_PGS = 'hdmv_pgs_subtitle'
TRACK_TYPE_KEY = 'track_type'
DISPOSITION_SET_KEY = 'disposition_set'
TAGS_KEY = 'tags'
AUDIO_LAYOUT_KEY = 'audio_layout'
def __init__(self, **kwargs):
FFPROBE_DISPOSITION_KEY = 'disposition'
FFPROBE_TAGS_KEY = 'tags'
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
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.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.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 = ''
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.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 dict")
self.__subIndex = kwargs[TrackDescriptor.SUB_INDEX_KEY]
else:
self.__subIndex = -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 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.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.CODEC_NAME_KEY in kwargs.keys():
if type(kwargs[TrackDescriptor.CODEC_NAME_KEY]) is not str:
raise TypeError(
f"TrackDesciptor.__init__(): Argument {TrackDescriptor.CODEC_NAME_KEY} is required to be of type str"
)
self.__codecName = kwargs[TrackDescriptor.CODEC_NAME_KEY]
else:
self.__codecName = ''
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.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.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
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"
}
}
"""
@classmethod
def fromFfprobe(cls, streamObj):
"""Processes ffprobe stream data as array with elements according to the following example
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.CODEC_NAME_KEY] = str(streamObj[TrackDescriptor.FFPROBE_CODEC_NAME_KEY])
kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = (
{
"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"
t
for d in (
k
for (k, v) in streamObj[
TrackDescriptor.FFPROBE_DISPOSITION_KEY
].items()
if v
)
if (t := TrackDisposition.find(d)) 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
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 = {}
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = trackType
kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = {t for d in (k for (k,v) in streamObj[TrackDescriptor.FFPROBE_DISPOSITION_KEY].items() if v)
if (t := TrackDisposition.find(d)) if t is not None} if TrackDescriptor.FFPROBE_DISPOSITION_KEY in streamObj.keys() else set()
kwargs[TrackDescriptor.TAGS_KEY] = streamObj[TrackDescriptor.FFPROBE_TAGS_KEY] if TrackDescriptor.FFPROBE_TAGS_KEY in streamObj.keys() else {}
kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = AudioLayout.identify(streamObj) if trackType == TrackType.AUDIO.label() else AudioLayout.LAYOUT_UNDEFINED
def getPatternId(self):
return self.__patternId
return cls(**kwargs)
else:
return None
def getIndex(self):
return self.__index
def setIndex(self, index):
self.__index = index
def getId(self):
return self.__trackId
def getSourceIndex(self):
return self.__sourceIndex
def getPatternId(self):
return self.__patternId
def getSubIndex(self):
return self.__subIndex
def getIndex(self):
return self.__index
def setSubIndex(self, subIndex):
self.__subIndex = subIndex
def getSubIndex(self):
return self.__subIndex
def getType(self):
return self.__trackType
def getCodec(self):
return self.__codecName
def getLanguage(self):
if "language" in self.__trackTags.keys():
return IsoLanguage.findThreeLetter(self.__trackTags["language"])
else:
return IsoLanguage.UNDEFINED
def getType(self):
return self.__trackType
def getTitle(self):
if "title" in self.__trackTags.keys():
return str(self.__trackTags["title"])
else:
return ""
def getLanguage(self):
if 'language' in self.__trackTags.keys():
return IsoLanguage.findThreeLetter(self.__trackTags['language'])
else:
return IsoLanguage.UNDEFINED
def getAudioLayout(self):
return self.__audioLayout
def getTitle(self):
if 'title' in self.__trackTags.keys():
return str(self.__trackTags['title'])
else:
return ''
def getTags(self):
return self.__trackTags
def getAudioLayout(self):
return self.__audioLayout
def getDispositionSet(self):
return self.__dispositionSet
def getDispositionFlag(self, disposition: TrackDisposition) -> bool:
return bool(disposition in self.__dispositionSet)
def getTags(self):
return self.__trackTags
def getDispositionSet(self):
return self.__dispositionSet
def setDispositionFlag(self, disposition: TrackDisposition, state: bool):
if state:
self.__dispositionSet.add(disposition)
else:
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

@@ -7,6 +7,7 @@ from textual.widgets import Header, Footer, Static, Button, SelectionList, Selec
from textual.containers import Grid
from ffx.model.pattern import Pattern
from ffx.model.track import Track
from .track_controller import TrackController
from .pattern_controller import PatternController
@@ -32,9 +33,9 @@ class TrackDetailsScreen(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;
grid-size: 5 24;
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 125;
height: 100%;
width: 100%;
padding: 1;
@@ -79,9 +80,13 @@ class TrackDetailsScreen(Screen):
height: 100%;
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__()
self.context = self.app.getContext()
@@ -94,11 +99,15 @@ class TrackDetailsScreen(Screen):
self.__isNew = trackDescriptor is None
if self.__isNew:
self.__trackType = trackType
self.__audioLayout = AudioLayout.LAYOUT_UNDEFINED
self.__index = index
self.__subIndex = subIndex
self.__trackDescriptor : TrackDescriptor = None
self.__pattern : Pattern = self.__pc.getPattern(patternId) if patternId is not None else {}
else:
self.__trackType = trackDescriptor.getType()
self.__audioLayout = trackDescriptor.getAudioLayout()
self.__index = trackDescriptor.getIndex()
self.__subIndex = trackDescriptor.getSubIndex()
self.__trackDescriptor : TrackDescriptor = trackDescriptor
self.__pattern : Pattern = self.__pc.getPattern(self.__trackDescriptor.getPatternId())
@@ -124,11 +133,16 @@ class TrackDetailsScreen(Screen):
def on_mount(self):
if self.__pattern is not None:
self.query_one("#patternlabel", Static).update(self.__pattern.getPattern())
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.__subIndex is not None:
self.query_one("#subindexlabel", Static).update(str(self.__subIndex))
if self.__pattern is not None:
self.query_one("#pattern_label", Static).update(self.__pattern.getPattern())
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:
@@ -151,7 +165,7 @@ class TrackDetailsScreen(Screen):
# Define the columns with headers
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'
@@ -163,71 +177,87 @@ class TrackDetailsScreen(Screen):
with Grid():
# 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
yield Static("for pattern")
yield Static("", id="patternlabel", classes="four")
yield Static("", id="pattern_label", classes="four")
# 3
yield Static("sub index")
yield Static("", id="subindexlabel", classes="four")
yield Static(" ", classes="five")
# 4
yield Static(" ", classes="five")
yield Static("Index / Subindex")
yield Static("", id="index_label", classes="two")
yield Static("", id="subindex_label", classes="two")
# 5
yield Static(" ", classes="five")
# 6
yield Static("Language")
yield Select.from_values(languages, classes="four", id="language_select")
yield Static("Type")
yield Select.from_values([t.label() for t in TrackType], classes="four", id="type_select")
# 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
yield Static("Title")
yield Input(id="title_input", classes="four")
yield Static(" ", classes="five")
# 9
yield Static(" ", classes="five")
# 10
yield Static("Language")
yield Select.from_values(languages, classes="four", id="language_select")
# 11
yield Static(" ", classes="five")
# 11
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")
# 12
yield self.trackTagsTable
yield Static("Title")
yield Input(id="title_input", classes="four")
# 13
yield Static(" ", classes="five")
# 14
yield Static("Stream dispositions", classes="five")
yield Static(" ", classes="five")
# 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](
classes="five",
id = "dispositions_selection_list"
)
# 16
# 20
yield Static(" ", classes="five")
# 17
# 21
yield Static(" ", classes="five")
# 18
# 22
yield Button("Save", id="save_button")
yield Button("Cancel", id="cancel_button")
# 19
# 23
yield Static(" ", classes="five")
# 20
# 24
yield Static(" ", classes="five", id="messagestatic")
@@ -240,10 +270,11 @@ class TrackDetailsScreen(Screen):
kwargs[TrackDescriptor.PATTERN_ID_KEY] = int(self.__pattern.getId())
kwargs[TrackDescriptor.INDEX_KEY] = -1
kwargs[TrackDescriptor.SUB_INDEX_KEY] = self.__subIndex
kwargs[TrackDescriptor.INDEX_KEY] = self.__index
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 = {}
language = self.query_one("#language_select", Select).value
@@ -260,8 +291,6 @@ class TrackDetailsScreen(Screen):
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.AUDIO_LAYOUT_KEY] = AudioLayout.LAYOUT_UNDEFINED
return TrackDescriptor(**kwargs)
@@ -296,10 +325,10 @@ class TrackDetailsScreen(Screen):
# Check if the button pressed is the one we are interested in
if event.button.id == "save_button":
trackDescriptor = self.getTrackDescriptorFromInput()
# Check for multiple default/forced disposition flags
if self.__trackType == TrackType.VIDEO:
trackList = self.__tc.findVideoTracks(self.__pattern.getId())
if self.__trackType == TrackType.AUDIO:
trackList = self.__tc.findAudioTracks(self.__pattern.getId())
elif self.__trackType == TrackType.SUBTITLE:
@@ -307,29 +336,34 @@ class TrackDetailsScreen(Screen):
else:
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()])
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)
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")
else:
self.query_one("#messagestatic", Static).update(" ")
if self.__isNew:
# Track per Screen hinzufügen
self.__tc.addTrack(trackDescriptor)
self.dismiss(trackDescriptor)
else:
track = self.__tc.findTrack(self.__pattern.getId(), self.__trackType, self.__subIndex)
track = self.__tc.getTrack(self.__pattern.getId(), self.__index)
# Track per details screen updaten
if self.__tc.updateTrack(track.getId(), trackDescriptor):
self.dismiss(trackDescriptor)
@@ -358,7 +392,7 @@ class TrackDetailsScreen(Screen):
trackId = self.__trackDescriptor.getId()
if trackId == -1:
raise click.ClickException(f"TrackDetailsScreen.handle_add_tag: trackId not set (-1) trackDescriptor={self.__trackDescriptor}")
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()

View File

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