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.
262 lines
13 KiB
Python
262 lines
13 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
|
|
from ffx.ffx_controller import FfxController
|
|
from ffx.show_descriptor import ShowDescriptor
|
|
|
|
|
|
class FileProperties():
|
|
|
|
FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm']
|
|
|
|
SE_INDICATOR_PATTERN = '([sS][0-9]+[eE][0-9]+)'
|
|
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)'
|
|
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
|
|
|
|
DEFAULT_INDEX_DIGITS = 3
|
|
|
|
def __init__(self, context, sourcePath):
|
|
|
|
self.context = context
|
|
|
|
self.__logger = context['logger']
|
|
|
|
# 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 = ''
|
|
|
|
|
|
self.__pc = PatternController(context)
|
|
|
|
# db pattern boruto_[sS]([0-9]+)[eE]([0-9]+).mkv
|
|
|
|
# Checking if database contains matching pattern
|
|
matchResult = self.__pc.matchFilename(self.__sourceFilename)
|
|
|
|
self.__logger.debug(f"FileProperties.__init__(): Match result: {matchResult}")
|
|
|
|
self.__pattern: Pattern = matchResult['pattern'] if matchResult else None
|
|
|
|
if matchResult:
|
|
databaseMatchedGroups = matchResult['match'].groups()
|
|
self.__logger.debug(f"FileProperties.__init__(): Matched groups: {databaseMatchedGroups}")
|
|
|
|
seIndicator = databaseMatchedGroups[0]
|
|
|
|
se_match = re.search(FileProperties.SEASON_EPISODE_INDICATOR_MATCH, seIndicator)
|
|
e_match = re.search(FileProperties.EPISODE_INDICATOR_MATCH, seIndicator)
|
|
|
|
else:
|
|
self.__logger.debug(f"FileProperties.__init__(): Checking file name for indicator {self.__sourceFilename}")
|
|
|
|
se_match = re.search(FileProperties.SEASON_EPISODE_INDICATOR_MATCH, self.__sourceFilename)
|
|
e_match = re.search(FileProperties.EPISODE_INDICATOR_MATCH, self.__sourceFilename)
|
|
|
|
if se_match is not None:
|
|
self.__season = int(se_match.group(1))
|
|
self.__episode = int(se_match.group(2))
|
|
elif e_match is not None:
|
|
self.__season = -1
|
|
self.__episode = int(e_match.group(1))
|
|
else:
|
|
self.__season = -1
|
|
self.__episode = -1
|
|
|
|
|
|
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 getMediaDescriptor(self):
|
|
return MediaDescriptor.fromFfprobe(self.context, self.getFormatData(), self.getStreamData())
|
|
|
|
|
|
def getShowId(self) -> int:
|
|
"""Result is -1 if the filename did not match anything in database"""
|
|
return self.__pattern.getShowId() if self.__pattern is not None else -1
|
|
|
|
def getPattern(self) -> Pattern:
|
|
"""Result is None if the filename did not match anything in database"""
|
|
return self.__pattern
|
|
|
|
|
|
def getSeason(self):
|
|
return int(self.__season)
|
|
|
|
def getEpisode(self):
|
|
return int(self.__episode)
|
|
|
|
|
|
def getFilename(self):
|
|
return self.__sourceFilename
|
|
|
|
def getFileBasename(self):
|
|
return self.__sourceFileBasename
|
|
|
|
|
|
def assembleTargetFileBasename(self,
|
|
label: str = "",
|
|
quality: int = -1,
|
|
fileIndex: int = -1,
|
|
indexDigits: int = DEFAULT_INDEX_DIGITS,
|
|
extraTokens: list = []):
|
|
|
|
if 'show_descriptor' in self.context.keys():
|
|
season_digits = self.context['show_descriptor'][ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY]
|
|
episode_digits = self.context['show_descriptor'][ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY]
|
|
else:
|
|
season_digits = ShowDescriptor.DEFAULT_INDICATOR_SEASON_DIGITS
|
|
episode_digits = ShowDescriptor.DEFAULT_INDICATOR_EPISODE_DIGITS
|
|
|
|
targetFilenameTokens = []
|
|
|
|
# targetFilenameExtension = FfxController.DEFAULT_FILE_EXTENSION if extension is None else str(extension)
|
|
|
|
self.__logger.debug(f"assembleTargetFileBasename(): label={label} is {'truthy' if label else 'falsy'}")
|
|
|
|
if label:
|
|
|
|
targetFilenameTokens = [label]
|
|
|
|
if fileIndex > -1:
|
|
targetFilenameTokens += [f"{fileIndex:0{indexDigits}d}"]
|
|
elif self.__season > -1 and self.__episode > -1:
|
|
targetFilenameTokens += [f"S{self.__season:0{season_digits}d}E{self.__episode:0{episode_digits}d}"]
|
|
elif self.__episode > -1:
|
|
targetFilenameTokens += [f"E{self.__episode:0{episode_digits}d}"]
|
|
|
|
else:
|
|
targetFilenameTokens = [self.__sourceFileBasename]
|
|
|
|
|
|
if quality != -1:
|
|
targetFilenameTokens += [f"q{quality}"]
|
|
|
|
# In case source and target filenames are the same add an extension to distinct output from input
|
|
#if not label and self.__sourceFilenameExtension == targetFilenameExtension:
|
|
# targetFilenameTokens += ['ffx']
|
|
targetFilenameTokens += extraTokens
|
|
|
|
targetFilename = '_'.join(targetFilenameTokens)
|
|
|
|
self.__logger.debug(f"assembleTargetFileBasename(): Target filename: {targetFilename}")
|
|
|
|
return targetFilename
|