click-textual
Maveno 1 year ago
parent 5e017a8373
commit eaee3b34da

@ -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)
except Exception as ex:
raise click.ClickException(f"This file does not contain any audiovisual data: {ex}")
for d in sd:
click.echo(f"{d['codec_name']}{' (' + str(d['channels']) + ')' if d['codec_type'] == 'audio' else ''}")
fp = FileProperties(filename)
md = fp.getMediaDescriptor()
print(md.getTags())
@ffx.command() for at in md.getAudioTracks():
@click.pass_context print(f"Audio: {at.getLanguage()} {'|'.join([f"{k}={v}" for (k,v) in at.getTags().items()])}")
@click.argument('paths', nargs=-1) for st in md.getSubtitleTracks():
@click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix') print(f"Subtitle: {st.getLanguage()} {'|'.join([[f"{k}={v}" for (k,v) in st.getTags().items()]])}")
@click.option('-sd', '--subtitle-directory', type=str, default='', help='Load subtitles from here')
@click.option('-sp', '--subtitle-prefix', type=str, default='', help='Subtitle filename prefix')
@click.option("-o", "--output-directory", type=str, default='')
@click.option("--dry-run", is_flag=True, default=False)
except Exception as ex:
raise click.ClickException(f"This file does not contain any audiovisual data: {ex}")
def unmux(ctx, # for d in sd:
label, # click.echo(f"{d['codec_name']}{' (' + str(d['channels']) + ')' if d['codec_type'] == 'audio' else ''}")
paths,
subtitle_directory,
subtitle_prefix,
output_directory,
dry_run):
existingSourcePaths = [p for p in paths if os.path.isfile(p)]
click.echo(f"\nUnmuxing {len(existingSourcePaths)} files")
for sourcePath in existingSourcePaths:
sd = getStreamDescriptor(sourcePath)
print(f"\nFile {sourcePath}\n")
for v in sd['video']:
if v['codec_name'] == 'h264':
commandSequence = ['ffmpeg', '-i', sourcePath, '-map', '0:v:0', '-c', 'copy', '-f', 'h264']
executeProcess()
for a in sd['audio']: # @ffx.command()
print(f"A: {a}\n") # @click.pass_context
for s in sd['subtitle']: #
print(f"S: {s}\n") # @click.argument('paths', nargs=-1)
# @click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix')
#
# @click.option('-sd', '--subtitle-directory', type=str, default='', help='Load subtitles from here')
# @click.option('-sp', '--subtitle-prefix', type=str, default='', help='Subtitle filename prefix')
#
# @click.option("-o", "--output-directory", type=str, default='')
#
# @click.option("--dry-run", is_flag=True, default=False)
#
#
# def unmux(ctx,
# label,
# paths,
# subtitle_directory,
# subtitle_prefix,
# output_directory,
# dry_run):
#
# existingSourcePaths = [p for p in paths if os.path.isfile(p)]
# click.echo(f"\nUnmuxing {len(existingSourcePaths)} files")
#
# for sourcePath in existingSourcePaths:
#
# sd = getStreamDescriptor(sourcePath)
#
# print(f"\nFile {sourcePath}\n")
#
# for v in sd['video']:
#
# if v['codec_name'] == 'h264':
#
# commandSequence = ['ffmpeg', '-i', sourcePath, '-map', '0:v:0', '-c', 'copy', '-f', 'h264']
# executeProcess()
#
# for a in sd['audio']:
# print(f"A: {a}\n")
# for s in sd['subtitle']:
# print(f"S: {s}\n")

@ -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 {})
for streamObj in streamData:
md.appendTrack(streamObj) # formatData = self.getFormatData()
# streamData = self.getStreamData()
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 [] @classmethod
# self.__audioTracks = mediaDescriptor[TrackType.AUDIO.label()] if TrackType.AUDIO.label() in mediaDescriptor.keys() else [] def fromFfprobe(cls, formatData, streamData):
# self.__subtitleTracks = mediaDescriptor[TrackType.SUBTITLE.label()] if TrackType.SUBTITLE.label() in mediaDescriptor.keys() else []
self.__clearTags = clear_tags descriptors = {}
for t in tracks: for streamObj in streamData:
self.appendTrack(t)
trackType = TrackType.fromLabel(streamObj['codec_type'])
def appendTrack(self, trackDescriptor): if trackType != TrackType.UNKNOWN:
ttype = trackDescriptor['type'].label() if trackType.label() not in descriptors.keys():
descriptors[trackType.label()] = []
if ttype not in self.__tracks.keys():
self.__tracks[ttype] = [] 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 []
self.__tracks[ttype] = trackDescriptor 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,11 +176,13 @@ 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():
FFPROBE_DISPOSITION_KEY = 'disposition'
FFPROBE_TAGS_KEY = 'tags'
def __init__(self, **kwargs):
def getTrack(self): # self.__index = int(kwargs['index']) if 'index' in kwargs.keys() else -1
pass # 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'])
def index(self):
"""Returns the stream type index"""
return int(self.value['index'])
labels = { @staticmethod
TrackType.VIDEO: "video", def fromLabel(label):
TrackType.AUDIO: "audio", tlist = [t for t in TrackType if t.value['label'] == label]
TrackType.SUBTITLE: "subtitle" if tlist:
} return tlist[0]
else:
return TrackType.UNKNOWN
return labels.get(self, "undefined") @staticmethod
def fromIndex(index):
tlist = [t for t in TrackType if t.value['index'] == index]
if tlist:
return tlist[0]
else:
return TrackType.UNKNOWN

Loading…
Cancel
Save