nightl
This commit is contained in:
118
bin/ffx.py
118
bin/ffx.py
@@ -41,11 +41,6 @@ STREAM_TYPE_VIDEO = 'video'
|
|||||||
STREAM_TYPE_AUDIO = 'audio'
|
STREAM_TYPE_AUDIO = 'audio'
|
||||||
STREAM_TYPE_SUBTITLE = 'subtitle'
|
STREAM_TYPE_SUBTITLE = 'subtitle'
|
||||||
|
|
||||||
STREAM_LAYOUT_6_1 = '6.1'
|
|
||||||
STREAM_LAYOUT_5_1 = '5.1(side)'
|
|
||||||
STREAM_LAYOUT_STEREO = 'stereo'
|
|
||||||
STREAM_LAYOUT_6CH = '6ch'
|
|
||||||
|
|
||||||
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)'
|
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)'
|
||||||
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
|
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
|
||||||
SEASON_EPISODE_STREAM_LANGUAGE_MATCH = '[sS]([0-9]+)[eE]([0-9]+)_([0-9]+)_([a-z]{3})'
|
SEASON_EPISODE_STREAM_LANGUAGE_MATCH = '[sS]([0-9]+)[eE]([0-9]+)_([0-9]+)_([a-z]{3})'
|
||||||
@@ -53,119 +48,6 @@ SEASON_EPISODE_STREAM_LANGUAGE_MATCH = '[sS]([0-9]+)[eE]([0-9]+)_([0-9]+)_([a-z]
|
|||||||
SUBTITLE_FILE_EXTENSION = 'vtt'
|
SUBTITLE_FILE_EXTENSION = 'vtt'
|
||||||
|
|
||||||
|
|
||||||
def executeProcess(commandSequence):
|
|
||||||
|
|
||||||
process = subprocess.Popen(commandSequence, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
|
|
||||||
output, error = process.communicate()
|
|
||||||
|
|
||||||
return output.decode('utf-8'), error.decode('utf-8'), process.returncode
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[{'index': 0, 'codec_name': 'vp9', 'codec_long_name': 'Google VP9', 'profile': 'Profile 0', 'codec_type': 'video', 'codec_tag_string': '[0][0][0][0]', 'codec_tag': '0x0000', 'width': 1920, 'height': 1080, 'coded_width': 1920, 'coded_height': 1080, 'closed_captions': 0, 'film_grain': 0, 'has_b_frames': 0, 'sample_aspect_ratio': '1:1', 'display_aspect_ratio': '16:9', 'pix_fmt': 'yuv420p', 'level': -99, 'color_range': 'tv', 'chroma_location': 'left', 'field_order': 'progressive', 'refs': 1, 'r_frame_rate': '24000/1001', 'avg_frame_rate': '24000/1001', 'time_base': '1/1000', 'start_pts': 0, 'start_time': '0.000000', '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': {'BPS': '7974017', 'NUMBER_OF_FRAMES': '34382', 'NUMBER_OF_BYTES': '1429358655', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 libvpx-vp9', 'DURATION': '00:23:54.016000000'}}]
|
|
||||||
#[{'index': 1, 'codec_name': 'opus', 'codec_long_name': 'Opus (Opus Interactive Audio Codec)', 'codec_type': 'audio', 'codec_tag_string': '[0][0][0][0]', 'codec_tag': '0x0000', 'sample_fmt': 'fltp', 'sample_rate': '48000', 'channels': 2, 'channel_layout': 'stereo', 'bits_per_sample': 0, 'initial_padding': 312, 'r_frame_rate': '0/0', 'avg_frame_rate': '0/0', 'time_base': '1/1000', 'start_pts': -7, 'start_time': '-0.007000', 'extradata_size': 19, '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': 'jpn', 'title': 'Japanisch', 'BPS': '128000', 'NUMBER_OF_FRAMES': '61763', 'NUMBER_OF_BYTES': '22946145', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 libopus', 'DURATION': '00:23:54.141000000'}}]
|
|
||||||
|
|
||||||
#[{'index': 2, 'codec_name': 'webvtt', 'codec_long_name': 'WebVTT subtitle', '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': -7, 'start_time': '-0.007000', 'duration_ts': 1434141, 'duration': '1434.141000', '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': 'Deutsch [Full]', 'BPS': '118', 'NUMBER_OF_FRAMES': '300', 'NUMBER_OF_BYTES': '21128', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 webvtt', 'DURATION': '00:23:54.010000000'}}, {'index': 3, 'codec_name': 'webvtt', 'codec_long_name': 'WebVTT subtitle', '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': -7, 'start_time': '-0.007000', 'duration_ts': 1434141, 'duration': '1434.141000', 'disposition': {'default': 0, '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': 'eng', 'title': 'Englisch [Full]', 'BPS': '101', 'NUMBER_OF_FRAMES': '276', 'NUMBER_OF_BYTES': '16980', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 webvtt', 'DURATION': '00:23:53.230000000'}}]
|
|
||||||
|
|
||||||
|
|
||||||
def getStreamData(filepath):
|
|
||||||
"""Returns 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
ffprobeOutput, ffprobeError, returnCode = executeProcess(["ffprobe",
|
|
||||||
"-show_streams",
|
|
||||||
"-of", "json",
|
|
||||||
filepath])
|
|
||||||
|
|
||||||
if 'Invalid data found when processing input' in ffprobeError:
|
|
||||||
raise Exception(f"File {filepath} does not contain valid stream data")
|
|
||||||
|
|
||||||
if returnCode != 0:
|
|
||||||
raise Exception(f"ffprobe returned with error {returnCode}")
|
|
||||||
|
|
||||||
return json.loads(ffprobeOutput)['streams']
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def getStreamDescriptor(filename):
|
|
||||||
|
|
||||||
streamData = getStreamData(filename)
|
|
||||||
|
|
||||||
descriptor = {}
|
|
||||||
descriptor['video'] = []
|
|
||||||
descriptor['audio'] = []
|
|
||||||
descriptor['subtitle'] = []
|
|
||||||
|
|
||||||
for subStream in streamData:
|
|
||||||
|
|
||||||
if subStream['codec_type'] in ['video', 'audio', 'subtitle']:
|
|
||||||
|
|
||||||
if not 'disposition' in subStream.keys():
|
|
||||||
subStream['disposition'] = {}
|
|
||||||
if not 'default' in subStream['disposition'].keys():
|
|
||||||
subStream['disposition']['default'] = 0
|
|
||||||
if not 'forced' in subStream['disposition'].keys():
|
|
||||||
subStream['disposition']['forced'] = 0
|
|
||||||
if not 'tags' in subStream.keys():
|
|
||||||
subStream['tags'] = {}
|
|
||||||
if not 'language' in subStream['tags'].keys():
|
|
||||||
subStream['tags']['language'] = 'undefined'
|
|
||||||
if not 'title' in subStream['tags'].keys():
|
|
||||||
subStream['tags']['title'] = 'undefined'
|
|
||||||
|
|
||||||
if subStream['codec_type'] == STREAM_TYPE_AUDIO:
|
|
||||||
if 'channel_layout' in subStream.keys():
|
|
||||||
subStream['audio_layout'] = subStream['channel_layout']
|
|
||||||
elif subStream['channels'] == 6:
|
|
||||||
subStream['audio_layout'] = STREAM_LAYOUT_6CH
|
|
||||||
else:
|
|
||||||
subStream['audio_layout'] = 'undefined'
|
|
||||||
|
|
||||||
descriptor[subStream['codec_type']].append(subStream)
|
|
||||||
descriptor[subStream['codec_type']][-1]['sub_index'] = len(descriptor[subStream['codec_type']]) - 1
|
|
||||||
|
|
||||||
return descriptor
|
|
||||||
|
|
||||||
|
|
||||||
def getModifiedStreamOrder(length, last):
|
def getModifiedStreamOrder(length, last):
|
||||||
"""This is jellyfin specific as the last stream in the order is set as default"""
|
"""This is jellyfin specific as the last stream in the order is set as default"""
|
||||||
|
|||||||
46
bin/ffx/audio_layout.py
Normal file
46
bin/ffx/audio_layout.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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_6CH = {"layout": "6ch", "index": 5}
|
||||||
|
|
||||||
|
LAYOUT_UNDEFINED = {"layout": "undefined", "index": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def layout(self):
|
||||||
|
"""Returns the layout as string"""
|
||||||
|
return self.value['layout']
|
||||||
|
|
||||||
|
def index(self):
|
||||||
|
"""Returns the layout as string"""
|
||||||
|
return self.value['layout']
|
||||||
|
|
||||||
|
|
||||||
|
def identify(self, streamObj):
|
||||||
|
|
||||||
|
FFPROBE_LAYOUT_KEY = 'channel_layout'
|
||||||
|
FFPROBE_CHANNELS_KEY = 'channels'
|
||||||
|
FFPROBE_CODEC_TYPE_KEY = 'codec_type'
|
||||||
|
|
||||||
|
if (type(streamObj) is not dict
|
||||||
|
or FFPROBE_CODEC_TYPE_KEY not in streamObj.keys()
|
||||||
|
or streamObj[FFPROBE_CODEC_TYPE_KEY] != TrackType.AUDIO.label()):
|
||||||
|
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]]
|
||||||
|
if matchingLayouts:
|
||||||
|
return matchingLayouts[0]
|
||||||
|
|
||||||
|
if (FFPROBE_CHANNELS_KEY in streamObj.keys()
|
||||||
|
and int(streamObj[FFPROBE_CHANNELS_KEY]) == 6):
|
||||||
|
|
||||||
|
return AudioLayout.LAYOUT_6CH
|
||||||
|
|
||||||
|
return AudioLayout.LAYOUT_UNDEFINED
|
||||||
@@ -10,6 +10,9 @@ from ffx.model.show import Base, Show
|
|||||||
from ffx.model.pattern import Pattern
|
from ffx.model.pattern import Pattern
|
||||||
from ffx.model.track import Track
|
from ffx.model.track import Track
|
||||||
|
|
||||||
|
from ffx.model.media_tag import MediaTag
|
||||||
|
from ffx.model.track_tag import TrackTag
|
||||||
|
|
||||||
from .shows_screen import ShowsScreen
|
from .shows_screen import ShowsScreen
|
||||||
from .warning_screen import WarningScreen
|
from .warning_screen import WarningScreen
|
||||||
from .dashboard_screen import DashboardScreen
|
from .dashboard_screen import DashboardScreen
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
import os, re, click
|
import os, re, click, json
|
||||||
|
|
||||||
|
from .media_descriptor import MediaDescriptor
|
||||||
|
|
||||||
|
from .track_type import TrackType
|
||||||
|
from .audio_layout import AudioLayout
|
||||||
|
|
||||||
|
from .track_disposition import TrackDisposition
|
||||||
|
|
||||||
|
from .process import executeProcess
|
||||||
|
|
||||||
|
|
||||||
class FileProperties():
|
class FileProperties():
|
||||||
|
|
||||||
@@ -7,15 +17,18 @@ class FileProperties():
|
|||||||
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)'
|
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)'
|
||||||
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
|
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
|
||||||
|
|
||||||
|
|
||||||
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.__sourceDirectory = os.path.dirname(sourcePath)
|
self.__sourcePath = sourcePath
|
||||||
self.__sourceFilename = os.path.basename(sourcePath)
|
|
||||||
|
self.__sourceDirectory = os.path.dirname(self.__sourcePath)
|
||||||
|
self.__sourceFilename = os.path.basename(self.__sourcePath)
|
||||||
|
|
||||||
sourcePathTokens = self.__sourceFilename.split('.')
|
sourcePathTokens = self.__sourceFilename.split('.')
|
||||||
|
|
||||||
if sourcePathTokens[-1] in FilenameController.FILE_EXTENSIONS:
|
if sourcePathTokens[-1] in FileProperties.FILE_EXTENSIONS:
|
||||||
self.__sourceFileBasename = '.'.join(sourcePathTokens[:-1])
|
self.__sourceFileBasename = '.'.join(sourcePathTokens[:-1])
|
||||||
self.__sourceFilenameExtension = sourcePathTokens[-1]
|
self.__sourceFilenameExtension = sourcePathTokens[-1]
|
||||||
else:
|
else:
|
||||||
@@ -23,8 +36,8 @@ class FileProperties():
|
|||||||
self.__sourceFilenameExtension = ''
|
self.__sourceFilenameExtension = ''
|
||||||
|
|
||||||
|
|
||||||
se_match = re.compile(FilenameController.SEASON_EPISODE_INDICATOR_MATCH)
|
se_match = re.compile(FileProperties.SEASON_EPISODE_INDICATOR_MATCH)
|
||||||
e_match = re.compile(FilenameController.EPISODE_INDICATOR_MATCH)
|
e_match = re.compile(FileProperties.EPISODE_INDICATOR_MATCH)
|
||||||
|
|
||||||
se_result = se_match.search(self.__sourceFilename)
|
se_result = se_match.search(self.__sourceFilename)
|
||||||
e_result = e_match.search(self.__sourceFilename)
|
e_result = e_match.search(self.__sourceFilename)
|
||||||
@@ -64,3 +77,145 @@ class FileProperties():
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
targetFilenameTokens = [sourceFileBasename]
|
targetFilenameTokens = [sourceFileBasename]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def getFormatData(self):
|
||||||
|
"""
|
||||||
|
"format": {
|
||||||
|
"filename": "Downloads/nagatoro_s02/nagatoro_s01e02.mkv",
|
||||||
|
"nb_streams": 18,
|
||||||
|
"nb_programs": 0,
|
||||||
|
"nb_stream_groups": 0,
|
||||||
|
"format_name": "matroska,webm",
|
||||||
|
"format_long_name": "Matroska / WebM",
|
||||||
|
"start_time": "0.000000",
|
||||||
|
"duration": "1420.063000",
|
||||||
|
"size": "1489169824",
|
||||||
|
"bit_rate": "8389316",
|
||||||
|
"probe_score": 100,
|
||||||
|
"tags": {
|
||||||
|
"PUBLISHER": "Crunchyroll",
|
||||||
|
"ENCODER": "Lavf58.29.100"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
ffprobeOutput, ffprobeError, returnCode = executeProcess(["ffprobe",
|
||||||
|
"-hide_banner",
|
||||||
|
"-show_format",
|
||||||
|
"-of", "json",
|
||||||
|
self.__sourcePath])
|
||||||
|
|
||||||
|
|
||||||
|
if 'Invalid data found when processing input' in ffprobeError:
|
||||||
|
raise Exception(f"File {self.__sourcePath} does not contain valid stream data")
|
||||||
|
|
||||||
|
|
||||||
|
if returnCode != 0:
|
||||||
|
raise Exception(f"ffprobe returned with error {returnCode}")
|
||||||
|
|
||||||
|
|
||||||
|
return json.loads(ffprobeOutput)['format']
|
||||||
|
|
||||||
|
#[{'index': 0, 'codec_name': 'vp9', 'codec_long_name': 'Google VP9', 'profile': 'Profile 0', 'codec_type': 'video', 'codec_tag_string': '[0][0][0][0]', 'codec_tag': '0x0000', 'width': 1920, 'height': 1080, 'coded_width': 1920, 'coded_height': 1080, 'closed_captions': 0, 'film_grain': 0, 'has_b_frames': 0, 'sample_aspect_ratio': '1:1', 'display_aspect_ratio': '16:9', 'pix_fmt': 'yuv420p', 'level': -99, 'color_range': 'tv', 'chroma_location': 'left', 'field_order': 'progressive', 'refs': 1, 'r_frame_rate': '24000/1001', 'avg_frame_rate': '24000/1001', 'time_base': '1/1000', 'start_pts': 0, 'start_time': '0.000000', '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': {'BPS': '7974017', 'NUMBER_OF_FRAMES': '34382', 'NUMBER_OF_BYTES': '1429358655', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 libvpx-vp9', 'DURATION': '00:23:54.016000000'}}]
|
||||||
|
#[{'index': 1, 'codec_name': 'opus', 'codec_long_name': 'Opus (Opus Interactive Audio Codec)', 'codec_type': 'audio', 'codec_tag_string': '[0][0][0][0]', 'codec_tag': '0x0000', 'sample_fmt': 'fltp', 'sample_rate': '48000', 'channels': 2, 'channel_layout': 'stereo', 'bits_per_sample': 0, 'initial_padding': 312, 'r_frame_rate': '0/0', 'avg_frame_rate': '0/0', 'time_base': '1/1000', 'start_pts': -7, 'start_time': '-0.007000', 'extradata_size': 19, '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': 'jpn', 'title': 'Japanisch', 'BPS': '128000', 'NUMBER_OF_FRAMES': '61763', 'NUMBER_OF_BYTES': '22946145', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 libopus', 'DURATION': '00:23:54.141000000'}}]
|
||||||
|
|
||||||
|
#[{'index': 2, 'codec_name': 'webvtt', 'codec_long_name': 'WebVTT subtitle', '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': -7, 'start_time': '-0.007000', 'duration_ts': 1434141, 'duration': '1434.141000', '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': 'Deutsch [Full]', 'BPS': '118', 'NUMBER_OF_FRAMES': '300', 'NUMBER_OF_BYTES': '21128', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 webvtt', 'DURATION': '00:23:54.010000000'}}, {'index': 3, 'codec_name': 'webvtt', 'codec_long_name': 'WebVTT subtitle', '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': -7, 'start_time': '-0.007000', 'duration_ts': 1434141, 'duration': '1434.141000', 'disposition': {'default': 0, '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': 'eng', 'title': 'Englisch [Full]', 'BPS': '101', 'NUMBER_OF_FRAMES': '276', 'NUMBER_OF_BYTES': '16980', '_STATISTICS_WRITING_APP': "mkvmerge v63.0.0 ('Everything') 64-bit", '_STATISTICS_WRITING_DATE_UTC': '2023-10-07 13:59:46', '_STATISTICS_TAGS': 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES', 'ENCODER': 'Lavc61.3.100 webvtt', 'DURATION': '00:23:53.230000000'}}]
|
||||||
|
|
||||||
|
|
||||||
|
def getStreamData(self):
|
||||||
|
"""Returns 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
ffprobeOutput, ffprobeError, returnCode = executeProcess(["ffprobe",
|
||||||
|
"-hide_banner",
|
||||||
|
"-show_streams",
|
||||||
|
"-of", "json",
|
||||||
|
self.__sourcePath])
|
||||||
|
|
||||||
|
if 'Invalid data found when processing input' in ffprobeError:
|
||||||
|
raise Exception(f"File {self.__sourcePath} does not contain valid stream data")
|
||||||
|
|
||||||
|
|
||||||
|
if returnCode != 0:
|
||||||
|
raise Exception(f"ffprobe returned with error {returnCode}")
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
|
||||||
|
formatData = self.getFormatData()
|
||||||
|
streamData = self.getStreamData()
|
||||||
|
|
||||||
|
md = MediaDescriptor(tags=formatData['tags'] if 'tags' in formatData.keys() else {})
|
||||||
|
|
||||||
|
for streamObj in streamData:
|
||||||
|
|
||||||
|
md.appendTrack(streamObj)
|
||||||
|
|
||||||
|
return md
|
||||||
|
|||||||
@@ -1,2 +1,29 @@
|
|||||||
|
from ffx.track_type import TrackType
|
||||||
|
|
||||||
class MediaDescriptor():
|
class MediaDescriptor():
|
||||||
pass
|
|
||||||
|
def __init__(self, tags = {}, clear_tags = False, tracks = []):
|
||||||
|
|
||||||
|
# self.__metaTags = mediaDescriptor['tags'] if 'tags' in mediaDescriptor.keys() else {}
|
||||||
|
self.__tags = tags
|
||||||
|
|
||||||
|
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):
|
||||||
|
|
||||||
|
ttype = trackDescriptor['type'].label()
|
||||||
|
|
||||||
|
if ttype not in self.__tracks.keys():
|
||||||
|
self.__tracks[ttype] = []
|
||||||
|
|
||||||
|
self.__tracks[ttype] = trackDescriptor
|
||||||
|
|||||||
28
bin/ffx/model/media_tag.py
Normal file
28
bin/ffx/model/media_tag.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# from typing import List
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey, Enum
|
||||||
|
from sqlalchemy.orm import relationship, declarative_base, sessionmaker
|
||||||
|
|
||||||
|
from .show import Base
|
||||||
|
|
||||||
|
|
||||||
|
class MediaTag(Base):
|
||||||
|
"""
|
||||||
|
relationship(argument, opt1, opt2, ...)
|
||||||
|
argument is string of class or Mapped class of the target entity
|
||||||
|
backref creates a bi-directional corresponding relationship (back_populates preferred)
|
||||||
|
back_populates points to the corresponding relationship (the actual class attribute identifier)
|
||||||
|
|
||||||
|
See: https://docs.sqlalchemy.org/en/(14|20)/orm/basic_relationships.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = 'media_tags'
|
||||||
|
|
||||||
|
# v1.x
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
|
||||||
|
key = Column(String)
|
||||||
|
value = Column(String)
|
||||||
|
|
||||||
|
# v1.x
|
||||||
|
pattern_id = Column(Integer, ForeignKey('patterns.id', ondelete="CASCADE"))
|
||||||
|
pattern = relationship('Pattern', back_populates='media_tags')
|
||||||
@@ -4,6 +4,7 @@ from sqlalchemy.orm import relationship, sessionmaker, Mapped, backref
|
|||||||
from .show import Base
|
from .show import Base
|
||||||
from .track import Track
|
from .track import Track
|
||||||
|
|
||||||
|
from ffx.media_descriptor import MediaDescriptor
|
||||||
|
|
||||||
class Pattern(Base):
|
class Pattern(Base):
|
||||||
|
|
||||||
@@ -26,3 +27,30 @@ class Pattern(Base):
|
|||||||
# show: Mapped["Show"] = relationship(back_populates="patterns")
|
# show: Mapped["Show"] = relationship(back_populates="patterns")
|
||||||
|
|
||||||
tracks = relationship('Track', back_populates='pattern', cascade="all, delete")
|
tracks = relationship('Track', back_populates='pattern', cascade="all, delete")
|
||||||
|
|
||||||
|
|
||||||
|
media_tags = relationship('MediaTag', back_populates='pattern', cascade="all, delete")
|
||||||
|
|
||||||
|
|
||||||
|
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 getMediaDescriptor(self):
|
||||||
|
|
||||||
|
md = MediaDescriptor(tags = self.getDescriptor()['tags'])
|
||||||
|
|
||||||
|
for t in self.tracks:
|
||||||
|
md.appendTrack(t.getDescriptor())
|
||||||
|
|
||||||
|
return md
|
||||||
|
|||||||
@@ -39,3 +39,19 @@ class Show(Base):
|
|||||||
index_episode_digits = Column(Integer, default=2)
|
index_episode_digits = Column(Integer, default=2)
|
||||||
indicator_season_digits = Column(Integer, default=2)
|
indicator_season_digits = Column(Integer, default=2)
|
||||||
indicator_episode_digits = Column(Integer, default=2)
|
indicator_episode_digits = Column(Integer, default=2)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ from ffx.track_type import TrackType
|
|||||||
|
|
||||||
from ffx.iso_language import IsoLanguage
|
from ffx.iso_language import IsoLanguage
|
||||||
|
|
||||||
from ffx.model.tag import Tag
|
from ffx.track_disposition import TrackDisposition
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Track(Base):
|
class Track(Base):
|
||||||
@@ -39,11 +38,70 @@ class Track(Base):
|
|||||||
language = Column(String) # IsoLanguage threeLetter
|
language = Column(String) # IsoLanguage threeLetter
|
||||||
title = Column(String)
|
title = Column(String)
|
||||||
|
|
||||||
tags = relationship('Tag', back_populates='track', cascade="all, delete")
|
|
||||||
|
track_tags = relationship('TrackTag', back_populates='track', cascade="all, delete")
|
||||||
|
|
||||||
|
|
||||||
disposition_flags = Column(Integer)
|
disposition_flags = Column(Integer)
|
||||||
|
|
||||||
|
|
||||||
def getDescriptor(self):
|
def __init__(self, **kwargs):
|
||||||
pass
|
|
||||||
|
trackType = kwargs.pop('track_type', None)
|
||||||
|
if trackType is not None:
|
||||||
|
self.track_type = int(trackType.value)
|
||||||
|
|
||||||
|
language = kwargs.pop('language', None)
|
||||||
|
if language is not None:
|
||||||
|
self.language = str(language.threeLetter())
|
||||||
|
|
||||||
|
dispositionList = kwargs.pop('disposition_flags', None)
|
||||||
|
if dispositionList is not None:
|
||||||
|
self.disposition_flags = int(TrackDisposition.toFlags(dispositionList))
|
||||||
|
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# def getDescriptor(self):
|
||||||
|
#
|
||||||
|
# descriptor = {}
|
||||||
|
# descriptor['id'] = int(self.id)
|
||||||
|
# descriptor['pattern_id'] = int(self.pattern_id)
|
||||||
|
# descriptor['type'] = TrackType(self.track_type)
|
||||||
|
# descriptor['sub_index'] = int(self.sub_index)
|
||||||
|
#
|
||||||
|
# descriptor['language'] = IsoLanguage.findThreeLetter(self.language)
|
||||||
|
# descriptor['title'] = str(self.title)
|
||||||
|
#
|
||||||
|
# descriptor['disposition_list'] = TrackDisposition.toList(self.disposition_flags)
|
||||||
|
#
|
||||||
|
# descriptor['tags'] = {}
|
||||||
|
# for t in self.track_tags:
|
||||||
|
# descriptor['tags'][str(t.key)] = str(t.value)
|
||||||
|
#
|
||||||
|
# return descriptor
|
||||||
|
|
||||||
|
|
||||||
|
def getId(self):
|
||||||
|
return int(self.id)
|
||||||
|
|
||||||
|
def getPatternId(self):
|
||||||
|
return int(self.pattern_id)
|
||||||
|
|
||||||
|
def getType(self):
|
||||||
|
return TrackType(self.track_type)
|
||||||
|
|
||||||
|
def getSubIndex(self):
|
||||||
|
return int(self.sub_index)
|
||||||
|
|
||||||
|
def getLanguage(self):
|
||||||
|
return IsoLanguage.findThreeLetter(self.language)
|
||||||
|
|
||||||
|
def getTitle(self):
|
||||||
|
return str(self.title)
|
||||||
|
|
||||||
|
def getDispositionList(self):
|
||||||
|
return TrackDisposition.toList(self.disposition_flags)
|
||||||
|
|
||||||
|
def getTags(self):
|
||||||
|
return {str(k.value):str(v.value) for (k,v) in self.track_tags}
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ from sqlalchemy.orm import relationship, declarative_base, sessionmaker
|
|||||||
|
|
||||||
from .show import Base
|
from .show import Base
|
||||||
|
|
||||||
from ffx.track_type import TrackType
|
|
||||||
|
|
||||||
|
class TrackTag(Base):
|
||||||
class Tag(Base):
|
|
||||||
"""
|
"""
|
||||||
relationship(argument, opt1, opt2, ...)
|
relationship(argument, opt1, opt2, ...)
|
||||||
argument is string of class or Mapped class of the target entity
|
argument is string of class or Mapped class of the target entity
|
||||||
@@ -17,7 +15,7 @@ class Tag(Base):
|
|||||||
See: https://docs.sqlalchemy.org/en/(14|20)/orm/basic_relationships.html
|
See: https://docs.sqlalchemy.org/en/(14|20)/orm/basic_relationships.html
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__tablename__ = 'tags'
|
__tablename__ = 'track_tags'
|
||||||
|
|
||||||
# v1.x
|
# v1.x
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
@@ -27,4 +25,4 @@ class Tag(Base):
|
|||||||
|
|
||||||
# v1.x
|
# v1.x
|
||||||
track_id = Column(Integer, ForeignKey('tracks.id', ondelete="CASCADE"))
|
track_id = Column(Integer, ForeignKey('tracks.id', ondelete="CASCADE"))
|
||||||
track = relationship('Track', back_populates='tags')
|
track = relationship('Track', back_populates='track_tags')
|
||||||
@@ -2,6 +2,8 @@ import click, re
|
|||||||
|
|
||||||
from ffx.model.pattern import Pattern
|
from ffx.model.pattern import Pattern
|
||||||
|
|
||||||
|
from .media_descriptor import MediaDescriptor
|
||||||
|
|
||||||
|
|
||||||
class PatternController():
|
class PatternController():
|
||||||
|
|
||||||
@@ -77,14 +79,6 @@ class PatternController():
|
|||||||
s.close()
|
s.close()
|
||||||
|
|
||||||
|
|
||||||
def getPatternDict(self, pattern):
|
|
||||||
patternDescriptor = {}
|
|
||||||
patternDescriptor['id'] = int(pattern.id)
|
|
||||||
patternDescriptor['pattern'] = str(pattern.pattern)
|
|
||||||
patternDescriptor['show_id'] = int(pattern.show_id)
|
|
||||||
return patternDescriptor
|
|
||||||
|
|
||||||
|
|
||||||
def getPatternDescriptor(self, patternId):
|
def getPatternDescriptor(self, patternId):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -93,7 +87,8 @@ class PatternController():
|
|||||||
|
|
||||||
if q.count():
|
if q.count():
|
||||||
pattern = q.first()
|
pattern = q.first()
|
||||||
return self.getPatternDict(pattern)
|
#return self.getPatternDict(pattern)
|
||||||
|
return pattern.getDescriptor()
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise click.ClickException(f"PatternController.getPatternDescriptor(): {repr(ex)}")
|
raise click.ClickException(f"PatternController.getPatternDescriptor(): {repr(ex)}")
|
||||||
@@ -161,3 +156,19 @@ class PatternController():
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def getMediaDescriptor(self, patternId):
|
||||||
|
|
||||||
|
try:
|
||||||
|
s = self.Session()
|
||||||
|
q = s.query(Pattern).filter(Pattern.id == int(patternId))
|
||||||
|
|
||||||
|
if q.count():
|
||||||
|
pattern = q.first()
|
||||||
|
#return self.getPatternDict(pattern)
|
||||||
|
return pattern.getMediaDescriptor()
|
||||||
|
|
||||||
|
except Exception as ex:
|
||||||
|
raise click.ClickException(f"PatternController.getPatternDescriptor(): {repr(ex)}")
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
16
bin/ffx/process.py
Normal file
16
bin/ffx/process.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
#class ProcessController():
|
||||||
|
|
||||||
|
#def __init__(self, commandSequence):
|
||||||
|
# self.__commandSequence = commandSequence
|
||||||
|
|
||||||
|
def executeProcess(commandSequence):
|
||||||
|
|
||||||
|
process = subprocess.Popen(commandSequence, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
|
||||||
|
output, error = process.communicate()
|
||||||
|
|
||||||
|
return output.decode('utf-8'), error.decode('utf-8'), process.returncode
|
||||||
|
|
||||||
@@ -22,16 +22,19 @@ class ShowController():
|
|||||||
if q.count():
|
if q.count():
|
||||||
show = q.first()
|
show = q.first()
|
||||||
|
|
||||||
showDescriptor['id'] = int(show.id)
|
# showDescriptor['id'] = int(show.id)
|
||||||
showDescriptor['name'] = str(show.name)
|
# showDescriptor['name'] = str(show.name)
|
||||||
showDescriptor['year'] = int(show.year)
|
# showDescriptor['year'] = int(show.year)
|
||||||
|
#
|
||||||
|
# showDescriptor['index_season_digits'] = int(show.index_season_digits)
|
||||||
|
# showDescriptor['index_episode_digits'] = int(show.index_episode_digits)
|
||||||
|
# showDescriptor['indicator_season_digits'] = int(show.indicator_season_digits)
|
||||||
|
# showDescriptor['indicator_episode_digits'] = int(show.indicator_episode_digits)
|
||||||
|
#
|
||||||
|
# return showDescriptor
|
||||||
|
|
||||||
showDescriptor['index_season_digits'] = int(show.index_season_digits)
|
return show.getDesciptor()
|
||||||
showDescriptor['index_episode_digits'] = int(show.index_episode_digits)
|
|
||||||
showDescriptor['indicator_season_digits'] = int(show.indicator_season_digits)
|
|
||||||
showDescriptor['indicator_episode_digits'] = int(show.indicator_episode_digits)
|
|
||||||
|
|
||||||
return showDescriptor
|
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise click.ClickException(f"ShowController.getShowDesciptor(): {repr(ex)}")
|
raise click.ClickException(f"ShowController.getShowDesciptor(): {repr(ex)}")
|
||||||
|
|||||||
@@ -118,18 +118,6 @@ class TrackController():
|
|||||||
s.close()
|
s.close()
|
||||||
|
|
||||||
|
|
||||||
def getTrackDict(self, track):
|
|
||||||
trackDescriptor = {}
|
|
||||||
trackDescriptor['id'] = int(track.id)
|
|
||||||
trackDescriptor['pattern_id'] = int(track.pattern_id)
|
|
||||||
trackDescriptor['type'] = TrackType(track.track_type)
|
|
||||||
trackDescriptor['sub_index'] = int(track.sub_index)
|
|
||||||
trackDescriptor['language'] = IsoLanguage.findThreeLetter(track.language)
|
|
||||||
trackDescriptor['title'] = str(track.title)
|
|
||||||
trackDescriptor['disposition_list'] = TrackDisposition.toList(track.disposition_flags)
|
|
||||||
return trackDescriptor
|
|
||||||
|
|
||||||
|
|
||||||
def getTrackDescriptor(self, trackId):
|
def getTrackDescriptor(self, trackId):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -138,7 +126,8 @@ class TrackController():
|
|||||||
|
|
||||||
if q.count():
|
if q.count():
|
||||||
track = q.first()
|
track = q.first()
|
||||||
return self.getTrackDict(track)
|
#return self.getTrackDict(track)
|
||||||
|
return track.getDescriptor()
|
||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +1,11 @@
|
|||||||
from language_data import LanguageData
|
from .iso_language import IsoLanguage
|
||||||
from stream_type import StreamType
|
from .track_type import TrackType
|
||||||
|
from .audio_layout import AudioLayout
|
||||||
|
|
||||||
class StreamDescriptor():
|
class StreamDescriptor():
|
||||||
|
pass
|
||||||
|
|
||||||
def __init__(self,
|
|
||||||
streamType : StreamType,
|
|
||||||
language : LanguageData,
|
|
||||||
title : str,
|
|
||||||
codec : str,
|
|
||||||
subIndex : int = -1):
|
|
||||||
|
|
||||||
self.__streamType = streamType
|
|
||||||
self.__subIndex = subIndex
|
|
||||||
|
|
||||||
self.__streamLanguage = language
|
def getTrack(self):
|
||||||
self.__streamTitle = title
|
pass
|
||||||
|
|
||||||
self.__codecName = codec
|
|
||||||
|
|
||||||
# "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"
|
|
||||||
|
|||||||
@@ -47,3 +47,11 @@ class TrackDisposition(Enum):
|
|||||||
if flags & int(2 ** d.index()):
|
if flags & int(2 ** d.index()):
|
||||||
dispositionList += [d]
|
dispositionList += [d]
|
||||||
return dispositionList
|
return dispositionList
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def find(disposition):
|
||||||
|
matchingDispositions = [d for d in TrackDisposition if d.label() == str(disposition)]
|
||||||
|
if matchingDispositions:
|
||||||
|
return matchingDispositions[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user