You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
222 lines
12 KiB
Python
222 lines
12 KiB
Python
import os, re, click, json
|
|
|
|
from .media_descriptor import MediaDescriptor
|
|
from .pattern_controller import PatternController
|
|
|
|
from .process import executeProcess
|
|
|
|
from ffx.model.pattern import Pattern
|
|
|
|
|
|
class FileProperties():
|
|
|
|
FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm']
|
|
|
|
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)'
|
|
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
|
|
|
|
|
|
def __init__(self, context, sourcePath):
|
|
|
|
# Separate basedir, basename and extension for current source file
|
|
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 FileProperties.FILE_EXTENSIONS:
|
|
self.__sourceFileBasename = '.'.join(sourcePathTokens[:-1])
|
|
self.__sourceFilenameExtension = sourcePathTokens[-1]
|
|
else:
|
|
self.__sourceFileBasename = self.__sourceFilename
|
|
self.__sourceFilenameExtension = ''
|
|
|
|
|
|
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)
|
|
|
|
self.__season = -1
|
|
self.__episode = -1
|
|
file_index = 0
|
|
|
|
if se_result is not None:
|
|
self.__season = int(se_result.group(1))
|
|
self.__episode = int(se_result.group(2))
|
|
elif e_result is not None:
|
|
self.__episode = int(e_result.group(1))
|
|
else:
|
|
file_index += 1
|
|
|
|
|
|
self.__pc = PatternController(context)
|
|
|
|
self.__pattern = self.__pc.matchFilename(self.__sourceFilename)
|
|
|
|
|
|
# click.echo(pattern)
|
|
# 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}")
|
|
#
|
|
#
|
|
# # Assemble target filename tokens
|
|
# targetFilenameTokens = []
|
|
# targetFilenameExtension = DEFAULT_FILE_EXTENSION
|
|
#
|
|
# if label:
|
|
# targetFilenameTokens = [label]
|
|
#
|
|
# if season > -1 and episode > -1:
|
|
# targetFilenameTokens += [f"S{season:0{season_digits}d}E{episode:0{episode_digits}d}"]
|
|
# elif episode > -1:
|
|
# targetFilenameTokens += [f"E{episode:0{episode_digits}d}"]
|
|
# else:
|
|
# targetFilenameTokens += [f"{file_index:0{index_digits}d}"]
|
|
#
|
|
# 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):
|
|
return MediaDescriptor.fromFfprobe(self.getFormatData(), self.getStreamData())
|
|
|
|
|
|
def getShowId(self) -> int:
|
|
return self.__pattern.getShowId() if self.__pattern is not None else -1
|
|
|
|
def getPattern(self) -> Pattern:
|
|
return self.__pattern
|
|
|