click-textual
Maveno 1 year ago
parent 123d8659e1
commit 5e017a8373

@ -41,11 +41,6 @@ STREAM_TYPE_VIDEO = 'video'
STREAM_TYPE_AUDIO = 'audio'
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]+)'
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
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'
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):
"""This is jellyfin specific as the last stream in the order is set as default"""

@ -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.track import Track
from ffx.model.media_tag import MediaTag
from ffx.model.track_tag import TrackTag
from .shows_screen import ShowsScreen
from .warning_screen import WarningScreen
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():
@ -7,15 +17,18 @@ class FileProperties():
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[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
self.__sourceDirectory = os.path.dirname(sourcePath)
self.__sourceFilename = os.path.basename(sourcePath)
self.__sourcePath = sourcePath
self.__sourceDirectory = os.path.dirname(self.__sourcePath)
self.__sourceFilename = os.path.basename(self.__sourcePath)
sourcePathTokens = self.__sourceFilename.split('.')
if sourcePathTokens[-1] in FilenameController.FILE_EXTENSIONS:
if sourcePathTokens[-1] in FileProperties.FILE_EXTENSIONS:
self.__sourceFileBasename = '.'.join(sourcePathTokens[:-1])
self.__sourceFilenameExtension = sourcePathTokens[-1]
else:
@ -23,8 +36,8 @@ class FileProperties():
self.__sourceFilenameExtension = ''
se_match = re.compile(FilenameController.SEASON_EPISODE_INDICATOR_MATCH)
e_match = re.compile(FilenameController.EPISODE_INDICATOR_MATCH)
se_match = re.compile(FileProperties.SEASON_EPISODE_INDICATOR_MATCH)
e_match = re.compile(FileProperties.EPISODE_INDICATOR_MATCH)
se_result = se_match.search(self.__sourceFilename)
e_result = e_match.search(self.__sourceFilename)
@ -64,3 +77,145 @@ class FileProperties():
else:
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():
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

@ -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 .track import Track
from ffx.media_descriptor import MediaDescriptor
class Pattern(Base):
@ -26,3 +27,30 @@ class Pattern(Base):
# show: Mapped["Show"] = relationship(back_populates="patterns")
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)
indicator_season_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.model.tag import Tag
from ffx.track_disposition import TrackDisposition
class Track(Base):
@ -39,11 +38,70 @@ class Track(Base):
language = Column(String) # IsoLanguage threeLetter
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)
def getDescriptor(self):
pass
def __init__(self, **kwargs):
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 ffx.track_type import TrackType
class Tag(Base):
class TrackTag(Base):
"""
relationship(argument, opt1, opt2, ...)
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
"""
__tablename__ = 'tags'
__tablename__ = 'track_tags'
# v1.x
id = Column(Integer, primary_key=True)
@ -27,4 +25,4 @@ class Tag(Base):
# v1.x
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 .media_descriptor import MediaDescriptor
class PatternController():
@ -77,14 +79,6 @@ class PatternController():
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):
try:
@ -93,7 +87,8 @@ class PatternController():
if q.count():
pattern = q.first()
return self.getPatternDict(pattern)
#return self.getPatternDict(pattern)
return pattern.getDescriptor()
except Exception as ex:
raise click.ClickException(f"PatternController.getPatternDescriptor(): {repr(ex)}")
@ -160,4 +155,20 @@ class PatternController():
s.close()
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()

@ -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():
show = q.first()
showDescriptor['id'] = int(show.id)
showDescriptor['name'] = str(show.name)
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)
# showDescriptor['id'] = int(show.id)
# showDescriptor['name'] = str(show.name)
# 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
return show.getDesciptor()
return showDescriptor
except Exception as ex:
raise click.ClickException(f"ShowController.getShowDesciptor(): {repr(ex)}")

@ -118,18 +118,6 @@ class TrackController():
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):
try:
@ -138,7 +126,8 @@ class TrackController():
if q.count():
track = q.first()
return self.getTrackDict(track)
#return self.getTrackDict(track)
return track.getDescriptor()
else:
return {}

@ -1,56 +1,11 @@
from language_data import LanguageData
from stream_type import StreamType
from .iso_language import IsoLanguage
from .track_type import TrackType
from .audio_layout import AudioLayout
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
self.__streamTitle = title
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"
def getTrack(self):
pass

@ -47,3 +47,11 @@ class TrackDisposition(Enum):
if flags & int(2 ** d.index()):
dispositionList += [d]
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

Loading…
Cancel
Save