inc
This commit is contained in:
20
bin/ffx.py
20
bin/ffx.py
@@ -3,7 +3,7 @@
|
|||||||
import os, sys, subprocess, json, click, time, re
|
import os, sys, subprocess, json, click, time, re
|
||||||
|
|
||||||
from ffx.ffx_app import FfxApp
|
from ffx.ffx_app import FfxApp
|
||||||
|
from ffx.ffx_controller import FfxController
|
||||||
from ffx.database import databaseContext
|
from ffx.database import databaseContext
|
||||||
|
|
||||||
|
|
||||||
@@ -169,14 +169,14 @@ def shows(ctx):
|
|||||||
@click.argument('paths', nargs=-1)
|
@click.argument('paths', nargs=-1)
|
||||||
@click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix')
|
@click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix')
|
||||||
|
|
||||||
@click.option('-v', '--video-encoder', type=str, default=DEFAULT_VIDEO_ENCODER, help=f"Target video encoder (vp9 or av1) default: {DEFAULT_VIDEO_ENCODER}")
|
@click.option('-v', '--video-encoder', type=str, default=FfxController.DEFAULT_VIDEO_ENCODER, help=f"Target video encoder (vp9 or av1) default: {FfxController.DEFAULT_VIDEO_ENCODER}")
|
||||||
|
|
||||||
@click.option('-q', '--quality', type=str, default=DEFAULT_QUALITY, help=f"Quality settings to be used with VP9 encoder (default: {DEFAULT_QUALITY})")
|
@click.option('-q', '--quality', type=str, default=FfxController.DEFAULT_QUALITY, help=f"Quality settings to be used with VP9 encoder (default: {FfxController.DEFAULT_QUALITY})")
|
||||||
@click.option('-p', '--preset', type=str, default=DEFAULT_AV1_PRESET, help=f"Quality preset to be used with AV1 encoder (default: {DEFAULT_AV1_PRESET})")
|
@click.option('-p', '--preset', type=str, default=FfxController.DEFAULT_AV1_PRESET, help=f"Quality preset to be used with AV1 encoder (default: {FfxController.DEFAULT_AV1_PRESET})")
|
||||||
|
|
||||||
@click.option('-a', '--stereo-bitrate', type=int, default=DEFAULT_STEREO_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode stereo audio streams (default: {DEFAULT_STEREO_BANDWIDTH})")
|
@click.option('-a', '--stereo-bitrate', type=int, default=FfxController.DEFAULT_STEREO_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode stereo audio streams (default: {FfxController.DEFAULT_STEREO_BANDWIDTH})")
|
||||||
@click.option('-ac3', '--ac3-bitrate', type=int, default=DEFAULT_AC3_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 5.1 audio streams (default: {DEFAULT_AC3_BANDWIDTH})")
|
@click.option('-ac3', '--ac3-bitrate', type=int, default=FfxController.DEFAULT_AC3_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 5.1 audio streams (default: {FfxController.DEFAULT_AC3_BANDWIDTH})")
|
||||||
@click.option('-dts', '--dts-bitrate', type=int, default=DEFAULT_DTS_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 6.1 audio streams (default: {DEFAULT_DTS_BANDWIDTH})")
|
@click.option('-dts', '--dts-bitrate', type=int, default=FfxController.DEFAULT_DTS_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 6.1 audio streams (default: {FfxController.DEFAULT_DTS_BANDWIDTH})")
|
||||||
|
|
||||||
@click.option('-sd', '--subtitle-directory', type=str, default='', help='Load subtitles from here')
|
@click.option('-sd', '--subtitle-directory', type=str, default='', help='Load subtitles from here')
|
||||||
@click.option('-sp', '--subtitle-prefix', type=str, default='', help='Subtitle filename prefix')
|
@click.option('-sp', '--subtitle-prefix', type=str, default='', help='Subtitle filename prefix')
|
||||||
@@ -297,8 +297,8 @@ def convert(ctx,
|
|||||||
if cTokens and len(cTokens) == 2:
|
if cTokens and len(cTokens) == 2:
|
||||||
cropStart, cropLength = crop.split(',')
|
cropStart, cropLength = crop.split(',')
|
||||||
else:
|
else:
|
||||||
cropStart = DEFAULT_CROP_START
|
cropStart = FfxController.DEFAULT_CROP_START
|
||||||
cropLength = DEFAULT_CROP_LENGTH
|
cropLength = FfxController.DEFAULT_CROP_LENGTH
|
||||||
|
|
||||||
click.echo(f"crop start={cropStart} length={cropLength}")
|
click.echo(f"crop start={cropStart} length={cropLength}")
|
||||||
|
|
||||||
@@ -359,7 +359,7 @@ def convert(ctx,
|
|||||||
|
|
||||||
# Assemble target filename tokens
|
# Assemble target filename tokens
|
||||||
targetFilenameTokens = []
|
targetFilenameTokens = []
|
||||||
targetFilenameExtension = DEFAULT_FILE_EXTENSION
|
targetFilenameExtension = FfxController.DEFAULT_FILE_EXTENSION
|
||||||
|
|
||||||
if label:
|
if label:
|
||||||
targetFilenameTokens = [label]
|
targetFilenameTokens = [label]
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ import click
|
|||||||
from typing import List, Self
|
from typing import List, Self
|
||||||
|
|
||||||
from ffx.track_type import TrackType
|
from ffx.track_type import TrackType
|
||||||
|
from ffx.track_disposition import TrackDisposition
|
||||||
|
|
||||||
from ffx.track_descriptor import TrackDescriptor
|
from ffx.track_descriptor import TrackDescriptor
|
||||||
|
|
||||||
from ffx.helper import dictDiff, DIFF_ADDED_KEY, DIFF_CHANGED_KEY, DIFF_REMOVED_KEY
|
from ffx.helper import dictDiff, DIFF_ADDED_KEY, DIFF_CHANGED_KEY, DIFF_REMOVED_KEY
|
||||||
|
|
||||||
|
|
||||||
class MediaDescriptor():
|
class MediaDescriptor():
|
||||||
"""This class represents the structural content of a media file including streams and metadata"""
|
"""This class represents the structural content of a media file including streams and metadata"""
|
||||||
|
|
||||||
@@ -14,11 +17,12 @@ class MediaDescriptor():
|
|||||||
TRACKS_KEY = 'tracks'
|
TRACKS_KEY = 'tracks'
|
||||||
|
|
||||||
TRACK_DESCRIPTOR_LIST_KEY = 'track_descriptors'
|
TRACK_DESCRIPTOR_LIST_KEY = 'track_descriptors'
|
||||||
CLEAR_TAGS_KEY = 'clear_tags'
|
CLEAR_TAGS_FLAG_KEY = 'clear_tags'
|
||||||
|
|
||||||
FFPROBE_DISPOSITION_KEY = 'disposition'
|
FFPROBE_DISPOSITION_KEY = 'disposition'
|
||||||
FFPROBE_TAGS_KEY = 'tags'
|
FFPROBE_TAGS_KEY = 'tags'
|
||||||
FFPROBE_CODEC_TYPE_KEY = 'codec_type'
|
FFPROBE_CODEC_TYPE_KEY = 'codec_type'
|
||||||
|
JELLYFIN_ORDER_FLAG_KEY = 'jellyfin_order'
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
|
||||||
@@ -42,13 +46,63 @@ class MediaDescriptor():
|
|||||||
else:
|
else:
|
||||||
self.__trackDescriptors = []
|
self.__trackDescriptors = []
|
||||||
|
|
||||||
if MediaDescriptor.CLEAR_TAGS_KEY in kwargs.keys():
|
if MediaDescriptor.CLEAR_TAGS_FLAG_KEY in kwargs.keys():
|
||||||
if type(kwargs[MediaDescriptor.CLEAR_TAGS_KEY]) is not bool:
|
if type(kwargs[MediaDescriptor.CLEAR_TAGS_FLAG_KEY]) is not bool:
|
||||||
raise TypeError(f"MediaDescriptor.__init__(): Argument {MediaDescriptor.CLEAR_TAGS_KEY} is required to be of type bool")
|
raise TypeError(f"MediaDescriptor.__init__(): Argument {MediaDescriptor.CLEAR_TAGS_FLAG_KEY} is required to be of type bool")
|
||||||
self.__clearTags = kwargs[MediaDescriptor.CLEAR_TAGS_KEY]
|
self.__clearTags = kwargs[MediaDescriptor.CLEAR_TAGS_FLAG_KEY]
|
||||||
else:
|
else:
|
||||||
self.__clearTags = False
|
self.__clearTags = False
|
||||||
|
|
||||||
|
if MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY in kwargs.keys():
|
||||||
|
if type(kwargs[MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY]) is not bool:
|
||||||
|
raise TypeError(f"MediaDescriptor.__init__(): Argument {MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY} is required to be of type bool")
|
||||||
|
self.__jellyfinOrder = kwargs[MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY]
|
||||||
|
else:
|
||||||
|
self.__jellyfinOrder = False
|
||||||
|
|
||||||
|
|
||||||
|
def __getReorderedTrackDescriptors(self):
|
||||||
|
|
||||||
|
videoTracks = [v for v in self.__trackDescriptors.copy() if v.getType() == TrackType.VIDEO]
|
||||||
|
audioTracks = [a for a in self.__trackDescriptors.copy() if a.getType() == TrackType.AUDIO]
|
||||||
|
subtitleTracks = [s for s in self.__trackDescriptors.copy() if s.getType() == TrackType.SUBTITLE]
|
||||||
|
|
||||||
|
videoDefaultTracks = [v for v in videoTracks if TrackDisposition.DEFAULT in v.getDispositionSet()]
|
||||||
|
videoForcedTracks = [v for v in videoTracks if TrackDisposition.FORCED in v.getDispositionSet()]
|
||||||
|
audioDefaultTracks = [a for a in audioTracks if TrackDisposition.DEFAULT in a.getDispositionSet()]
|
||||||
|
audioForcedTracks = [a for a in audioTracks if TrackDisposition.FORCED in a.getDispositionSet()]
|
||||||
|
subtitleDefaultTracks = [s for s in subtitleTracks if TrackDisposition.DEFAULT in s.getDispositionSet()]
|
||||||
|
subtitleForcedTracks = [s for s in subtitleTracks if TrackDisposition.FORCED in s.getDispositionSet()]
|
||||||
|
|
||||||
|
if len(videoDefaultTracks) > 1:
|
||||||
|
raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one default video track is not supported')
|
||||||
|
if len(videoForcedTracks) > 1:
|
||||||
|
raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one forced video track is not supported')
|
||||||
|
if len(audioDefaultTracks) > 1:
|
||||||
|
raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one default audio track is not supported')
|
||||||
|
if len(audioForcedTracks) > 1:
|
||||||
|
raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one forced audio track is not supported')
|
||||||
|
if len(subtitleDefaultTracks) > 1:
|
||||||
|
raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one default subtitle track is not supported')
|
||||||
|
if len(subtitleForcedTracks) > 1:
|
||||||
|
raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one forced subtitle track is not supported')
|
||||||
|
|
||||||
|
if self.__jellyfinOrder:
|
||||||
|
if videoDefaultTracks:
|
||||||
|
videoTracks.append(videoTracks.pop(videoTracks.index(videoDefaultTracks[0])))
|
||||||
|
if audioDefaultTracks:
|
||||||
|
audioTracks.append(audioTracks.pop(audioTracks.index(audioDefaultTracks[0])))
|
||||||
|
if subtitleDefaultTracks:
|
||||||
|
subtitleTracks.append(subtitleTracks.pop(subtitleTracks.index(subtitleDefaultTracks[0])))
|
||||||
|
|
||||||
|
reorderedTrackDescriptors = videoTracks + audioTracks + subtitleTracks
|
||||||
|
orderedSourceTrackSequence = [t.getSourceIndex() for t in reorderedTrackDescriptors]
|
||||||
|
|
||||||
|
if len(set(orderedSourceTrackSequence)) < len(orderedSourceTrackSequence):
|
||||||
|
raise ValueError(f"Multiple streams originating from the same source stream not supported")
|
||||||
|
|
||||||
|
return reorderedTrackDescriptors
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fromFfprobe(cls, formatData, streamData):
|
def fromFfprobe(cls, formatData, streamData):
|
||||||
@@ -72,7 +126,8 @@ class MediaDescriptor():
|
|||||||
if trackType not in subIndexCounters.keys():
|
if trackType not in subIndexCounters.keys():
|
||||||
subIndexCounters[trackType] = 0
|
subIndexCounters[trackType] = 0
|
||||||
|
|
||||||
kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY].append(TrackDescriptor.fromFfprobe(streamObj, subIndex=subIndexCounters[trackType]))
|
kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY].append(TrackDescriptor.fromFfprobe(streamObj,
|
||||||
|
subIndex=subIndexCounters[trackType]))
|
||||||
subIndexCounters[trackType] += 1
|
subIndexCounters[trackType] += 1
|
||||||
|
|
||||||
|
|
||||||
@@ -112,7 +167,8 @@ class MediaDescriptor():
|
|||||||
|
|
||||||
|
|
||||||
# Target track configuration (from DB)
|
# Target track configuration (from DB)
|
||||||
tracks = self.getAllTracks()
|
#tracks = self.getAllTracks()
|
||||||
|
tracks = self.__getReorderedTrackDescriptors()
|
||||||
numTracks = len(tracks)
|
numTracks = len(tracks)
|
||||||
|
|
||||||
# Current track configuration (of file)
|
# Current track configuration (of file)
|
||||||
@@ -134,6 +190,7 @@ class MediaDescriptor():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Will trigger if tracks are missing in DB definition
|
# Will trigger if tracks are missing in DB definition
|
||||||
|
# New tracks will be added per update via this way
|
||||||
if tp > (numTracks - 1):
|
if tp > (numTracks - 1):
|
||||||
if DIFF_REMOVED_KEY not in trackCompareResult.keys():
|
if DIFF_REMOVED_KEY not in trackCompareResult.keys():
|
||||||
trackCompareResult[DIFF_REMOVED_KEY] = {}
|
trackCompareResult[DIFF_REMOVED_KEY] = {}
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ class MediaDetailsScreen(Screen):
|
|||||||
# from file (=current) vs from stored in database (=target)
|
# from file (=current) vs from stored in database (=target)
|
||||||
self.__mediaDifferences = self.__targetMediaDescriptor.compare(self.__currentMediaDescriptor) if self.__currentPattern is not None else {}
|
self.__mediaDifferences = self.__targetMediaDescriptor.compare(self.__currentMediaDescriptor) if self.__currentPattern is not None else {}
|
||||||
|
|
||||||
|
#rtd = self.__targetMediaDescriptor.getReorderedTrackDescriptors()
|
||||||
|
#raise click.ClickException(f"getReorderedTrackDescriptors={[r.getIndex() for r in rtd]}")
|
||||||
|
|
||||||
def updateDifferences(self):
|
def updateDifferences(self):
|
||||||
|
|
||||||
@@ -242,6 +244,7 @@ class MediaDetailsScreen(Screen):
|
|||||||
row = (' ', '<New show>', ' ') # Convert each element to a string before adding
|
row = (' ', '<New show>', ' ') # Convert each element to a string before adding
|
||||||
self.showsTable.add_row(*map(str, row))
|
self.showsTable.add_row(*map(str, row))
|
||||||
|
|
||||||
|
#TODO: Stürzt ab wenn keine Shows vorhanden sind. Onthefly Show add impl
|
||||||
for show in self.__sc.getAllShows():
|
for show in self.__sc.getAllShows():
|
||||||
row = (int(show.id), show.name, show.year) # Convert each element to a string before adding
|
row = (int(show.id), show.name, show.year) # Convert each element to a string before adding
|
||||||
self.showsTable.add_row(*map(str, row))
|
self.showsTable.add_row(*map(str, row))
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class Track(Base):
|
|||||||
track_type = Column(Integer) # TrackType
|
track_type = Column(Integer) # TrackType
|
||||||
|
|
||||||
index = Column(Integer)
|
index = Column(Integer)
|
||||||
# sub_index = Column(Integer)
|
source_index = Column(Integer)
|
||||||
|
|
||||||
# v1.x
|
# v1.x
|
||||||
pattern_id = Column(Integer, ForeignKey('patterns.id', ondelete="CASCADE"))
|
pattern_id = Column(Integer, ForeignKey('patterns.id', ondelete="CASCADE"))
|
||||||
@@ -149,6 +149,9 @@ class Track(Base):
|
|||||||
def getIndex(self):
|
def getIndex(self):
|
||||||
return int(self.index) if self.index is not None else -1
|
return int(self.index) if self.index is not None else -1
|
||||||
|
|
||||||
|
def getSourceIndex(self):
|
||||||
|
return int(self.source_index) if self.source_index is not None else -1
|
||||||
|
|
||||||
def getLanguage(self):
|
def getLanguage(self):
|
||||||
tags = {t.key:t.value for t in self.track_tags}
|
tags = {t.key:t.value for t in self.track_tags}
|
||||||
return IsoLanguage.findThreeLetter(tags['language']) if 'language' in tags.keys() else IsoLanguage.UNDEFINED
|
return IsoLanguage.findThreeLetter(tags['language']) if 'language' in tags.keys() else IsoLanguage.UNDEFINED
|
||||||
@@ -182,6 +185,7 @@ class Track(Base):
|
|||||||
kwargs[TrackDescriptor.PATTERN_ID_KEY] = self.getPatternId()
|
kwargs[TrackDescriptor.PATTERN_ID_KEY] = self.getPatternId()
|
||||||
|
|
||||||
kwargs[TrackDescriptor.INDEX_KEY] = self.getIndex()
|
kwargs[TrackDescriptor.INDEX_KEY] = self.getIndex()
|
||||||
|
kwargs[TrackDescriptor.SOURCE_INDEX_KEY] = self.getSourceIndex()
|
||||||
|
|
||||||
if subIndex > -1:
|
if subIndex > -1:
|
||||||
kwargs[TrackDescriptor.SUB_INDEX_KEY] = subIndex
|
kwargs[TrackDescriptor.SUB_INDEX_KEY] = subIndex
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class TrackController():
|
|||||||
track = Track(pattern_id = patId,
|
track = Track(pattern_id = patId,
|
||||||
track_type = int(trackDescriptor.getType().index()),
|
track_type = int(trackDescriptor.getType().index()),
|
||||||
index = int(trackDescriptor.getIndex()),
|
index = int(trackDescriptor.getIndex()),
|
||||||
|
source_index = int(trackDescriptor.getSourceIndex()),
|
||||||
disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet())))
|
disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet())))
|
||||||
|
|
||||||
s.add(track)
|
s.add(track)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class TrackDescriptor():
|
|||||||
|
|
||||||
ID_KEY = 'id'
|
ID_KEY = 'id'
|
||||||
INDEX_KEY = 'index'
|
INDEX_KEY = 'index'
|
||||||
|
SOURCE_INDEX_KEY = 'source_index'
|
||||||
SUB_INDEX_KEY = 'sub_index'
|
SUB_INDEX_KEY = 'sub_index'
|
||||||
PATTERN_ID_KEY = 'pattern_id'
|
PATTERN_ID_KEY = 'pattern_id'
|
||||||
|
|
||||||
@@ -46,6 +47,11 @@ class TrackDescriptor():
|
|||||||
else:
|
else:
|
||||||
self.__index = -1
|
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 TrackDescriptor.SUB_INDEX_KEY in kwargs.keys():
|
||||||
if type(kwargs[TrackDescriptor.SUB_INDEX_KEY]) is not int:
|
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")
|
raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.SUB_INDEX_KEY} is required to be of type int")
|
||||||
@@ -136,6 +142,7 @@ class TrackDescriptor():
|
|||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
||||||
kwargs[TrackDescriptor.INDEX_KEY] = int(streamObj[TrackDescriptor.FFPROBE_INDEX_KEY]) if TrackDescriptor.FFPROBE_INDEX_KEY in streamObj.keys() else -1
|
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.SUB_INDEX_KEY] = subIndex
|
||||||
|
|
||||||
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = trackType
|
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = trackType
|
||||||
@@ -158,6 +165,10 @@ class TrackDescriptor():
|
|||||||
def getIndex(self):
|
def getIndex(self):
|
||||||
return self.__index
|
return self.__index
|
||||||
|
|
||||||
|
def getSourceIndex(self):
|
||||||
|
return self.__sourceIndex
|
||||||
|
|
||||||
|
|
||||||
def getSubIndex(self):
|
def getSubIndex(self):
|
||||||
return self.__subIndex
|
return self.__subIndex
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user