nightl
This commit is contained in:
174
bin/ffx.py
174
bin/ffx.py
@@ -4,6 +4,10 @@ import os, sys, subprocess, json, click, time, re
|
|||||||
|
|
||||||
from ffx.ffx_app import FfxApp
|
from ffx.ffx_app import FfxApp
|
||||||
|
|
||||||
|
from ffx.media_descriptor import MediaDescriptor
|
||||||
|
from ffx.file_properties import FileProperties
|
||||||
|
|
||||||
|
|
||||||
VERSION='0.1.0'
|
VERSION='0.1.0'
|
||||||
|
|
||||||
DEFAULT_VIDEO_ENCODER = 'vp9'
|
DEFAULT_VIDEO_ENCODER = 'vp9'
|
||||||
@@ -114,38 +118,39 @@ def generateOutputTokens(filepath, format, ext):
|
|||||||
|
|
||||||
def generateAudioEncodingTokens(context, index, layout):
|
def generateAudioEncodingTokens(context, index, layout):
|
||||||
"""Generates ffmpeg options for one output audio stream including channel remapping, codec and bitrate"""
|
"""Generates ffmpeg options for one output audio stream including channel remapping, codec and bitrate"""
|
||||||
|
pass
|
||||||
if layout == STREAM_LAYOUT_6_1:
|
#
|
||||||
return [f"-c:a:{index}",
|
# if layout == STREAM_LAYOUT_6_1:
|
||||||
'libopus',
|
# return [f"-c:a:{index}",
|
||||||
f"-filter:a:{index}",
|
# 'libopus',
|
||||||
'channelmap=channel_layout=6.1',
|
# f"-filter:a:{index}",
|
||||||
f"-b:a:{index}",
|
# 'channelmap=channel_layout=6.1',
|
||||||
context['bitrates']['dts']]
|
# f"-b:a:{index}",
|
||||||
|
# context['bitrates']['dts']]
|
||||||
elif layout == STREAM_LAYOUT_5_1:
|
#
|
||||||
return [f"-c:a:{index}",
|
# elif layout == STREAM_LAYOUT_5_1:
|
||||||
'libopus',
|
# return [f"-c:a:{index}",
|
||||||
f"-filter:a:{index}",
|
# 'libopus',
|
||||||
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
|
# f"-filter:a:{index}",
|
||||||
f"-b:a:{index}",
|
# "channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
|
||||||
context['bitrates']['ac3']]
|
# f"-b:a:{index}",
|
||||||
|
# context['bitrates']['ac3']]
|
||||||
elif layout == STREAM_LAYOUT_STEREO:
|
#
|
||||||
return [f"-c:a:{index}",
|
# elif layout == STREAM_LAYOUT_STEREO:
|
||||||
'libopus',
|
# return [f"-c:a:{index}",
|
||||||
f"-b:a:{index}",
|
# 'libopus',
|
||||||
context['bitrates']['stereo']]
|
# f"-b:a:{index}",
|
||||||
|
# context['bitrates']['stereo']]
|
||||||
elif layout == STREAM_LAYOUT_6CH:
|
#
|
||||||
return [f"-c:a:{index}",
|
# elif layout == STREAM_LAYOUT_6CH:
|
||||||
'libopus',
|
# return [f"-c:a:{index}",
|
||||||
f"-filter:a:{index}",
|
# 'libopus',
|
||||||
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
|
# f"-filter:a:{index}",
|
||||||
f"-b:a:{index}",
|
# "channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
|
||||||
context['bitrates']['ac3']]
|
# f"-b:a:{index}",
|
||||||
else:
|
# context['bitrates']['ac3']]
|
||||||
return []
|
# else:
|
||||||
|
# return []
|
||||||
|
|
||||||
|
|
||||||
def generateClearTokens(streams):
|
def generateClearTokens(streams):
|
||||||
@@ -233,61 +238,74 @@ def help():
|
|||||||
click.echo(f"Usage: ffx [input file] [output file] [vp9|av1] [q=[nn[,nn,...]]] [p=nn] [a=nnn[k]] [ac3=nnn[k]] [dts=nnn[k]] [crop]")
|
click.echo(f"Usage: ffx [input file] [output file] [vp9|av1] [q=[nn[,nn,...]]] [p=nn] [a=nnn[k]] [ac3=nnn[k]] [dts=nnn[k]] [crop]")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@click.argument('filename', nargs=1)
|
@click.argument('filename', nargs=1)
|
||||||
@ffx.command()
|
@ffx.command()
|
||||||
def streams(filename):
|
def inspect(filename):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sd = getStreamDescriptor(filename)
|
|
||||||
|
fp = FileProperties(filename)
|
||||||
|
md = fp.getMediaDescriptor()
|
||||||
|
|
||||||
|
print(md.getTags())
|
||||||
|
|
||||||
|
for at in md.getAudioTracks():
|
||||||
|
print(f"Audio: {at.getLanguage()} {'|'.join([f"{k}={v}" for (k,v) in at.getTags().items()])}")
|
||||||
|
|
||||||
|
for st in md.getSubtitleTracks():
|
||||||
|
print(f"Subtitle: {st.getLanguage()} {'|'.join([[f"{k}={v}" for (k,v) in st.getTags().items()]])}")
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise click.ClickException(f"This file does not contain any audiovisual data: {ex}")
|
raise click.ClickException(f"This file does not contain any audiovisual data: {ex}")
|
||||||
for d in sd:
|
|
||||||
click.echo(f"{d['codec_name']}{' (' + str(d['channels']) + ')' if d['codec_type'] == 'audio' else ''}")
|
# for d in sd:
|
||||||
|
# click.echo(f"{d['codec_name']}{' (' + str(d['channels']) + ')' if d['codec_type'] == 'audio' else ''}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ffx.command()
|
# @ffx.command()
|
||||||
@click.pass_context
|
# @click.pass_context
|
||||||
|
#
|
||||||
@click.argument('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('-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')
|
||||||
|
#
|
||||||
@click.option("-o", "--output-directory", type=str, default='')
|
# @click.option("-o", "--output-directory", type=str, default='')
|
||||||
|
#
|
||||||
@click.option("--dry-run", is_flag=True, default=False)
|
# @click.option("--dry-run", is_flag=True, default=False)
|
||||||
|
#
|
||||||
|
#
|
||||||
def unmux(ctx,
|
# def unmux(ctx,
|
||||||
label,
|
# label,
|
||||||
paths,
|
# paths,
|
||||||
subtitle_directory,
|
# subtitle_directory,
|
||||||
subtitle_prefix,
|
# subtitle_prefix,
|
||||||
output_directory,
|
# output_directory,
|
||||||
dry_run):
|
# dry_run):
|
||||||
|
#
|
||||||
existingSourcePaths = [p for p in paths if os.path.isfile(p)]
|
# existingSourcePaths = [p for p in paths if os.path.isfile(p)]
|
||||||
click.echo(f"\nUnmuxing {len(existingSourcePaths)} files")
|
# click.echo(f"\nUnmuxing {len(existingSourcePaths)} files")
|
||||||
|
#
|
||||||
for sourcePath in existingSourcePaths:
|
# for sourcePath in existingSourcePaths:
|
||||||
|
#
|
||||||
sd = getStreamDescriptor(sourcePath)
|
# sd = getStreamDescriptor(sourcePath)
|
||||||
|
#
|
||||||
print(f"\nFile {sourcePath}\n")
|
# print(f"\nFile {sourcePath}\n")
|
||||||
|
#
|
||||||
for v in sd['video']:
|
# for v in sd['video']:
|
||||||
|
#
|
||||||
if v['codec_name'] == 'h264':
|
# if v['codec_name'] == 'h264':
|
||||||
|
#
|
||||||
commandSequence = ['ffmpeg', '-i', sourcePath, '-map', '0:v:0', '-c', 'copy', '-f', 'h264']
|
# commandSequence = ['ffmpeg', '-i', sourcePath, '-map', '0:v:0', '-c', 'copy', '-f', 'h264']
|
||||||
executeProcess()
|
# executeProcess()
|
||||||
|
#
|
||||||
for a in sd['audio']:
|
# for a in sd['audio']:
|
||||||
print(f"A: {a}\n")
|
# print(f"A: {a}\n")
|
||||||
for s in sd['subtitle']:
|
# for s in sd['subtitle']:
|
||||||
print(f"S: {s}\n")
|
# print(f"S: {s}\n")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import os, re, click, json
|
|||||||
|
|
||||||
from .media_descriptor import MediaDescriptor
|
from .media_descriptor import MediaDescriptor
|
||||||
|
|
||||||
from .track_type import TrackType
|
#from .track_type import TrackType
|
||||||
from .audio_layout import AudioLayout
|
#from .audio_layout import AudioLayout
|
||||||
|
|
||||||
from .track_disposition import TrackDisposition
|
#from .track_disposition import TrackDisposition
|
||||||
|
|
||||||
from .process import executeProcess
|
from .process import executeProcess
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ class FileProperties():
|
|||||||
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
|
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
|
||||||
|
|
||||||
|
|
||||||
def ___init__(self, sourcePath, ):
|
def __init__(self, sourcePath):
|
||||||
|
|
||||||
# Separate basedir, basename and extension for current source file
|
# Separate basedir, basename and extension for current source file
|
||||||
self.__sourcePath = sourcePath
|
self.__sourcePath = sourcePath
|
||||||
@@ -55,29 +55,29 @@ class FileProperties():
|
|||||||
file_index += 1
|
file_index += 1
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
matchingFileSubtitleDescriptors = sorted([d for d in availableFileSubtitleDescriptors if d['season'] == season and d['episode'] == episode], key=lambda d: d['stream']) if availableFileSubtitleDescriptors else []
|
# matchingFileSubtitleDescriptors = sorted([d for d in availableFileSubtitleDescriptors if d['season'] == season and d['episode'] == episode], key=lambda d: d['stream']) if availableFileSubtitleDescriptors else []
|
||||||
|
#
|
||||||
print(f"season={season} episode={episode} file={file_index}")
|
# print(f"season={season} episode={episode} file={file_index}")
|
||||||
|
#
|
||||||
|
#
|
||||||
# Assemble target filename tokens
|
# # Assemble target filename tokens
|
||||||
targetFilenameTokens = []
|
# targetFilenameTokens = []
|
||||||
targetFilenameExtension = DEFAULT_FILE_EXTENSION
|
# targetFilenameExtension = DEFAULT_FILE_EXTENSION
|
||||||
|
#
|
||||||
if label:
|
# if label:
|
||||||
targetFilenameTokens = [label]
|
# targetFilenameTokens = [label]
|
||||||
|
#
|
||||||
if season > -1 and episode > -1:
|
# if season > -1 and episode > -1:
|
||||||
targetFilenameTokens += [f"S{season:0{season_digits}d}E{episode:0{episode_digits}d}"]
|
# targetFilenameTokens += [f"S{season:0{season_digits}d}E{episode:0{episode_digits}d}"]
|
||||||
elif episode > -1:
|
# elif episode > -1:
|
||||||
targetFilenameTokens += [f"E{episode:0{episode_digits}d}"]
|
# targetFilenameTokens += [f"E{episode:0{episode_digits}d}"]
|
||||||
else:
|
# else:
|
||||||
targetFilenameTokens += [f"{file_index:0{index_digits}d}"]
|
# targetFilenameTokens += [f"{file_index:0{index_digits}d}"]
|
||||||
|
#
|
||||||
else:
|
# else:
|
||||||
targetFilenameTokens = [sourceFileBasename]
|
# targetFilenameTokens = [sourceFileBasename]
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
def getFormatData(self):
|
def getFormatData(self):
|
||||||
@@ -185,37 +185,32 @@ class FileProperties():
|
|||||||
return json.loads(ffprobeOutput)['streams']
|
return json.loads(ffprobeOutput)['streams']
|
||||||
|
|
||||||
|
|
||||||
def getTrackDescriptor(self, streamObj):
|
# def getTrackDescriptor(self, streamObj):
|
||||||
"""Convert the stream describing json object into a track descriptor"""
|
# """Convert the stream describing json object into a track descriptor"""
|
||||||
|
#
|
||||||
trackType = streamObj['codec_type']
|
# trackType = streamObj['codec_type']
|
||||||
|
#
|
||||||
descriptor = {}
|
# descriptor = {}
|
||||||
|
#
|
||||||
if trackType in [t.label() for t in TrackType]:
|
# if trackType in [t.label() for t in TrackType]:
|
||||||
|
#
|
||||||
descriptor['type'] = trackType
|
# descriptor['type'] = trackType
|
||||||
|
#
|
||||||
descriptor = {}
|
# 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['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 {}
|
# descriptor['tags'] = streamObj['tags'] if 'tags' in streamObj.keys() else {}
|
||||||
|
#
|
||||||
if trackType == TrackType.AUDIO.label():
|
# if trackType == TrackType.AUDIO.label():
|
||||||
descriptor['layout'] = AudioLayout.identify(streamObj)
|
# descriptor['layout'] = AudioLayout.identify(streamObj)
|
||||||
|
#
|
||||||
return descriptor
|
# return descriptor
|
||||||
|
|
||||||
|
|
||||||
def getMediaDescriptor(self):
|
def getMediaDescriptor(self):
|
||||||
|
|
||||||
formatData = self.getFormatData()
|
return MediaDescriptor.fromFfprobe(self.getFormatData(), self.getStreamData())
|
||||||
streamData = self.getStreamData()
|
|
||||||
|
|
||||||
md = MediaDescriptor(tags=formatData['tags'] if 'tags' in formatData.keys() else {})
|
# formatData = self.getFormatData()
|
||||||
|
# streamData = self.getStreamData()
|
||||||
|
|
||||||
for streamObj in streamData:
|
|
||||||
|
|
||||||
md.appendTrack(streamObj)
|
|
||||||
|
|
||||||
return md
|
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ class IsoLanguage(Enum):
|
|||||||
VIETNAMESE = {"name": "Vietnamese", "iso639_1": "vi", "iso639_2": "vie"}
|
VIETNAMESE = {"name": "Vietnamese", "iso639_1": "vi", "iso639_2": "vie"}
|
||||||
WELSH = {"name": "Welsh", "iso639_1": "cy", "iso639_2": "wel"}
|
WELSH = {"name": "Welsh", "iso639_1": "cy", "iso639_2": "wel"}
|
||||||
|
|
||||||
|
UNDEFINED = {"name": "undefined", "iso639_1": "xx", "iso639_2": "und"}
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find(label : str):
|
def find(label : str):
|
||||||
@@ -81,14 +83,14 @@ class IsoLanguage(Enum):
|
|||||||
|
|
||||||
if closestMatches:
|
if closestMatches:
|
||||||
foundLangs = [l for l in IsoLanguage if l.value['name'] == closestMatches[0]]
|
foundLangs = [l for l in IsoLanguage if l.value['name'] == closestMatches[0]]
|
||||||
return foundLangs[0] if foundLangs else None
|
return foundLangs[0] if foundLangs else IsoLanguage.UNDEFINED
|
||||||
else:
|
else:
|
||||||
return None
|
return IsoLanguage.UNDEFINED
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def findThreeLetter(theeLetter : str):
|
def findThreeLetter(theeLetter : str):
|
||||||
foundLangs = [l for l in IsoLanguage if l.value['iso639_2'] == str(theeLetter)]
|
foundLangs = [l for l in IsoLanguage if l.value['iso639_2'] == str(theeLetter)]
|
||||||
return foundLangs[0] if foundLangs else None
|
return foundLangs[0] if foundLangs else IsoLanguage.UNDEFINED
|
||||||
|
|
||||||
|
|
||||||
# def get(lang : str):
|
# def get(lang : str):
|
||||||
|
|||||||
@@ -1,29 +1,43 @@
|
|||||||
from ffx.track_type import TrackType
|
from ffx.track_type import TrackType
|
||||||
|
from ffx.track_descriptor import TrackDescriptor
|
||||||
|
|
||||||
|
|
||||||
class MediaDescriptor():
|
class MediaDescriptor():
|
||||||
|
"""This class represents the structural content of a media file including streams and metadata"""
|
||||||
|
|
||||||
def __init__(self, tags = {}, clear_tags = False, tracks = []):
|
def __init__(self, **kwargs):
|
||||||
|
|
||||||
# self.__metaTags = mediaDescriptor['tags'] if 'tags' in mediaDescriptor.keys() else {}
|
self.__mediaTags = kwargs['tags'] if 'tags' in kwargs.keys() else {}
|
||||||
self.__tags = tags
|
self.__trackDescriptors = kwargs['trackDescriptors'] if 'trackDescriptors' in kwargs.keys() else {}
|
||||||
|
self.__clearTags = kwargs['clearTags'] if 'clearTags' in kwargs.keys() else False
|
||||||
self.__tracks = {}
|
|
||||||
|
|
||||||
# self.__videoTracks = mediaDescriptor[TrackType.VIDEO.label()] if TrackType.VIDEO.label() in mediaDescriptor.keys() else []
|
|
||||||
# self.__audioTracks = mediaDescriptor[TrackType.AUDIO.label()] if TrackType.AUDIO.label() in mediaDescriptor.keys() else []
|
|
||||||
# self.__subtitleTracks = mediaDescriptor[TrackType.SUBTITLE.label()] if TrackType.SUBTITLE.label() in mediaDescriptor.keys() else []
|
|
||||||
|
|
||||||
self.__clearTags = clear_tags
|
|
||||||
|
|
||||||
for t in tracks:
|
|
||||||
self.appendTrack(t)
|
|
||||||
|
|
||||||
|
|
||||||
def appendTrack(self, trackDescriptor):
|
@classmethod
|
||||||
|
def fromFfprobe(cls, formatData, streamData):
|
||||||
|
|
||||||
ttype = trackDescriptor['type'].label()
|
descriptors = {}
|
||||||
|
|
||||||
if ttype not in self.__tracks.keys():
|
for streamObj in streamData:
|
||||||
self.__tracks[ttype] = []
|
|
||||||
|
|
||||||
self.__tracks[ttype] = trackDescriptor
|
trackType = TrackType.fromLabel(streamObj['codec_type'])
|
||||||
|
|
||||||
|
if trackType != TrackType.UNKNOWN:
|
||||||
|
|
||||||
|
if trackType.label() not in descriptors.keys():
|
||||||
|
descriptors[trackType.label()] = []
|
||||||
|
|
||||||
|
descriptors[trackType.label()].append(TrackDescriptor.fromFfprobe(streamObj))
|
||||||
|
|
||||||
|
return cls(tags=formatData['tags'] if 'tags' in formatData.keys() else {},
|
||||||
|
trackDescriptors = descriptors)
|
||||||
|
|
||||||
|
|
||||||
|
def getTags(self):
|
||||||
|
return self.__mediaTags
|
||||||
|
|
||||||
|
|
||||||
|
def getAudioTracks(self):
|
||||||
|
return self.__trackDescriptors[TrackType.AUDIO.label()] if TrackType.AUDIO.label() in self.__trackDescriptors.keys() else []
|
||||||
|
|
||||||
|
def getSubtitleTracks(self):
|
||||||
|
return self.__trackDescriptors[TrackType.SUBTITLE.label()] if TrackType.SUBTITLE.label() in self.__trackDescriptors.keys() else []
|
||||||
|
|||||||
@@ -54,3 +54,16 @@ class Pattern(Base):
|
|||||||
md.appendTrack(t.getDescriptor())
|
md.appendTrack(t.getDescriptor())
|
||||||
|
|
||||||
return md
|
return md
|
||||||
|
|
||||||
|
|
||||||
|
def getId(self):
|
||||||
|
return int(self.id)
|
||||||
|
|
||||||
|
def getPattern(self):
|
||||||
|
return str(self.pattern)
|
||||||
|
|
||||||
|
def getShowId(self):
|
||||||
|
return int(self.show_id)
|
||||||
|
|
||||||
|
def getTags(self):
|
||||||
|
return {str(k.value):str(v.value) for (k,v) in self.media_tags}
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ class Track(Base):
|
|||||||
pattern = relationship('Pattern', back_populates='tracks')
|
pattern = relationship('Pattern', back_populates='tracks')
|
||||||
|
|
||||||
|
|
||||||
language = Column(String) # IsoLanguage threeLetter
|
# language = Column(String) # IsoLanguage threeLetter
|
||||||
title = Column(String)
|
# title = Column(String)
|
||||||
|
|
||||||
|
|
||||||
track_tags = relationship('TrackTag', back_populates='track', cascade="all, delete")
|
track_tags = relationship('TrackTag', back_populates='track', cascade="all, delete")
|
||||||
@@ -51,9 +51,9 @@ class Track(Base):
|
|||||||
if trackType is not None:
|
if trackType is not None:
|
||||||
self.track_type = int(trackType.value)
|
self.track_type = int(trackType.value)
|
||||||
|
|
||||||
language = kwargs.pop('language', None)
|
# language = kwargs.pop('language', None)
|
||||||
if language is not None:
|
# if language is not None:
|
||||||
self.language = str(language.threeLetter())
|
# self.language = str(language.threeLetter())
|
||||||
|
|
||||||
dispositionList = kwargs.pop('disposition_flags', None)
|
dispositionList = kwargs.pop('disposition_flags', None)
|
||||||
if dispositionList is not None:
|
if dispositionList is not None:
|
||||||
@@ -62,6 +62,87 @@ class Track(Base):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromStreamObj(cls, streamObj, subIndex, patternId):
|
||||||
|
"""{
|
||||||
|
'index': 4,
|
||||||
|
'codec_name': 'hdmv_pgs_subtitle',
|
||||||
|
'codec_long_name': 'HDMV Presentation Graphic Stream subtitles',
|
||||||
|
'codec_type': 'subtitle',
|
||||||
|
'codec_tag_string': '[0][0][0][0]',
|
||||||
|
'codec_tag': '0x0000',
|
||||||
|
'r_frame_rate': '0/0',
|
||||||
|
'avg_frame_rate': '0/0',
|
||||||
|
'time_base': '1/1000',
|
||||||
|
'start_pts': 0,
|
||||||
|
'start_time': '0.000000',
|
||||||
|
'duration_ts': 1421035,
|
||||||
|
'duration': '1421.035000',
|
||||||
|
'disposition': {
|
||||||
|
'default': 1,
|
||||||
|
'dub': 0,
|
||||||
|
'original': 0,
|
||||||
|
'comment': 0,
|
||||||
|
'lyrics': 0,
|
||||||
|
'karaoke': 0,
|
||||||
|
'forced': 0,
|
||||||
|
'hearing_impaired': 0,
|
||||||
|
'visual_impaired': 0,
|
||||||
|
'clean_effects': 0,
|
||||||
|
'attached_pic': 0,
|
||||||
|
'timed_thumbnails': 0,
|
||||||
|
'non_diegetic': 0,
|
||||||
|
'captions': 0,
|
||||||
|
'descriptions': 0,
|
||||||
|
'metadata': 0,
|
||||||
|
'dependent': 0,
|
||||||
|
'still_image': 0
|
||||||
|
},
|
||||||
|
'tags': {
|
||||||
|
'language': 'ger',
|
||||||
|
'title': 'German Full'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# v1.x
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement = True)
|
||||||
|
|
||||||
|
# P=pattern_id+sub_index+track_type
|
||||||
|
track_type = Column(Integer) # TrackType
|
||||||
|
sub_index = Column(Integer)
|
||||||
|
|
||||||
|
# v1.x
|
||||||
|
pattern_id = Column(Integer, ForeignKey('patterns.id', ondelete='CASCADE'))
|
||||||
|
pattern = relationship('Pattern', back_populates='tracks')
|
||||||
|
|
||||||
|
|
||||||
|
language = Column(String) # IsoLanguage threeLetter
|
||||||
|
title = Column(String)
|
||||||
|
|
||||||
|
|
||||||
|
track_tags = relationship('TrackTag', back_populates='track', cascade='all, delete')
|
||||||
|
|
||||||
|
|
||||||
|
disposition_flags = Column(Integer)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
trackType = streamObj['codec_type']
|
||||||
|
|
||||||
|
if trackType in [t.label() for t in TrackType]:
|
||||||
|
|
||||||
|
return cls(pattern_id = patternId,
|
||||||
|
sub_index = int(subIndex),
|
||||||
|
track_type = trackType,
|
||||||
|
disposition_flags = sum([2**t.index() for (k,v) in streamObj['disposition'].items() if v and (t := TrackDisposition.find(k)) is not None]))
|
||||||
|
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# def getDescriptor(self):
|
# def getDescriptor(self):
|
||||||
#
|
#
|
||||||
# descriptor = {}
|
# descriptor = {}
|
||||||
@@ -95,10 +176,12 @@ class Track(Base):
|
|||||||
return int(self.sub_index)
|
return int(self.sub_index)
|
||||||
|
|
||||||
def getLanguage(self):
|
def getLanguage(self):
|
||||||
return IsoLanguage.findThreeLetter(self.language)
|
tags = {t.key:t.value for t in self.track_tags}
|
||||||
|
return IsoLanguage.findThreeLetter(tags['language']) if 'language' in tags.keys() else IsoLanguage.UNKNOWN
|
||||||
|
|
||||||
def getTitle(self):
|
def getTitle(self):
|
||||||
return str(self.title)
|
tags = {t.key:t.value for t in self.track_tags}
|
||||||
|
return tags['title'] if 'title' in tags.keys() else ''
|
||||||
|
|
||||||
def getDispositionList(self):
|
def getDispositionList(self):
|
||||||
return TrackDisposition.toList(self.disposition_flags)
|
return TrackDisposition.toList(self.disposition_flags)
|
||||||
|
|||||||
@@ -1,11 +1,98 @@
|
|||||||
from .iso_language import IsoLanguage
|
from .iso_language import IsoLanguage
|
||||||
from .track_type import TrackType
|
from .track_type import TrackType
|
||||||
from .audio_layout import AudioLayout
|
from .audio_layout import AudioLayout
|
||||||
|
from .track_disposition import TrackDisposition
|
||||||
class StreamDescriptor():
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
|
class TrackDescriptor():
|
||||||
|
|
||||||
def getTrack(self):
|
FFPROBE_DISPOSITION_KEY = 'disposition'
|
||||||
pass
|
FFPROBE_TAGS_KEY = 'tags'
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
|
||||||
|
# self.__index = int(kwargs['index']) if 'index' in kwargs.keys() else -1
|
||||||
|
# self.__subIndex = int(kwargs['sub_index']) if 'sub_index' in kwargs.keys() else -1
|
||||||
|
|
||||||
|
self.__trackType = kwargs['trackType'] if 'trackType' in kwargs.keys() else TrackType.UNKNOWN
|
||||||
|
|
||||||
|
self.__trackTags = kwargs['tags'] if 'tags' in kwargs.keys() else {}
|
||||||
|
self.__dispositionSet = kwargs['dispositionSet'] if 'dispositionSet' in kwargs.keys() else set()
|
||||||
|
|
||||||
|
self.__audioLayout = kwargs['audioLayout'] if self.__trackType == TrackType.AUDIO and 'audioLayout' in kwargs.keys() else AudioLayout.LAYOUT_UNDEFINED
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromFfprobe(cls, streamObj):
|
||||||
|
"""Processes ffprobe stream data as array with elements according to the following example
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"codec_name": "hdmv_pgs_subtitle",
|
||||||
|
"codec_long_name": "HDMV Presentation Graphic Stream subtitles",
|
||||||
|
"codec_type": "subtitle",
|
||||||
|
"codec_tag_string": "[0][0][0][0]",
|
||||||
|
"codec_tag": "0x0000",
|
||||||
|
"r_frame_rate": "0/0",
|
||||||
|
"avg_frame_rate": "0/0",
|
||||||
|
"time_base": "1/1000",
|
||||||
|
"start_pts": 0,
|
||||||
|
"start_time": "0.000000",
|
||||||
|
"duration_ts": 1421035,
|
||||||
|
"duration": "1421.035000",
|
||||||
|
"disposition": {
|
||||||
|
"default": 1,
|
||||||
|
"dub": 0,
|
||||||
|
"original": 0,
|
||||||
|
"comment": 0,
|
||||||
|
"lyrics": 0,
|
||||||
|
"karaoke": 0,
|
||||||
|
"forced": 0,
|
||||||
|
"hearing_impaired": 0,
|
||||||
|
"visual_impaired": 0,
|
||||||
|
"clean_effects": 0,
|
||||||
|
"attached_pic": 0,
|
||||||
|
"timed_thumbnails": 0,
|
||||||
|
"non_diegetic": 0,
|
||||||
|
"captions": 0,
|
||||||
|
"descriptions": 0,
|
||||||
|
"metadata": 0,
|
||||||
|
"dependent": 0,
|
||||||
|
"still_image": 0
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"language": "ger",
|
||||||
|
"title": "German Full"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
trackType = TrackType.fromLabel(streamObj['codec_type']) if 'codec_type' in streamObj.keys() else TrackType.UNKNOWN
|
||||||
|
|
||||||
|
if trackType != TrackType.UNKNOWN:
|
||||||
|
|
||||||
|
return cls(trackType = trackType,
|
||||||
|
dispositionSet = {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(),
|
||||||
|
tags = streamObj[TrackDescriptor.FFPROBE_TAGS_KEY] if TrackDescriptor.FFPROBE_TAGS_KEY in streamObj.keys() else {},
|
||||||
|
audioLayout = AudioLayout.identify(streamObj) if trackType == TrackType.AUDIO.label() else AudioLayout.LAYOUT_UNDEFINED)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def getLanguage(self):
|
||||||
|
if 'language' in self.__trackTags.keys():
|
||||||
|
return IsoLanguage.findThreeLetter(self.__trackTags['language'])
|
||||||
|
else:
|
||||||
|
return IsoLanguage.UNKNOWN
|
||||||
|
|
||||||
|
def getTitle(self):
|
||||||
|
if 'title' in self.__trackTags.keys():
|
||||||
|
return str(self.__trackTags['title'])
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def getAudioLayout(self):
|
||||||
|
return self.__audioLayout
|
||||||
|
|
||||||
|
|
||||||
|
def getTags(self):
|
||||||
|
return self.__trackTags
|
||||||
@@ -49,8 +49,8 @@ class TrackDisposition(Enum):
|
|||||||
return dispositionList
|
return dispositionList
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find(disposition):
|
def find(label):
|
||||||
matchingDispositions = [d for d in TrackDisposition if d.label() == str(disposition)]
|
matchingDispositions = [d for d in TrackDisposition if d.label() == str(label)]
|
||||||
if matchingDispositions:
|
if matchingDispositions:
|
||||||
return matchingDispositions[0]
|
return matchingDispositions[0]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -2,18 +2,33 @@ from enum import Enum
|
|||||||
|
|
||||||
class TrackType(Enum):
|
class TrackType(Enum):
|
||||||
|
|
||||||
VIDEO = 1
|
VIDEO = {'label': 'video', 'index': 1}
|
||||||
AUDIO = 2
|
AUDIO = {'label': 'audio', 'index': 2}
|
||||||
SUBTITLE = 3
|
SUBTITLE = {'label': 'subtitle', 'index': 3}
|
||||||
|
|
||||||
|
UNKNOWN = {'label': 'unknown', 'index': 0}
|
||||||
|
|
||||||
|
|
||||||
def label(self):
|
def label(self):
|
||||||
"""Returns the stream type as string"""
|
"""Returns the stream type as string"""
|
||||||
|
return str(self.value['label'])
|
||||||
|
|
||||||
labels = {
|
def index(self):
|
||||||
TrackType.VIDEO: "video",
|
"""Returns the stream type index"""
|
||||||
TrackType.AUDIO: "audio",
|
return int(self.value['index'])
|
||||||
TrackType.SUBTITLE: "subtitle"
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels.get(self, "undefined")
|
@staticmethod
|
||||||
|
def fromLabel(label):
|
||||||
|
tlist = [t for t in TrackType if t.value['label'] == label]
|
||||||
|
if tlist:
|
||||||
|
return tlist[0]
|
||||||
|
else:
|
||||||
|
return TrackType.UNKNOWN
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromIndex(index):
|
||||||
|
tlist = [t for t in TrackType if t.value['index'] == index]
|
||||||
|
if tlist:
|
||||||
|
return tlist[0]
|
||||||
|
else:
|
||||||
|
return TrackType.UNKNOWN
|
||||||
|
|||||||
Reference in New Issue
Block a user