season/episode recognition

click-textual
Javanaut 12 months ago
parent 260c605201
commit 30d22892f8

@ -11,55 +11,6 @@ from ffx.database import databaseContext
VERSION='0.1.0' VERSION='0.1.0'
STREAM_TYPE_VIDEO = 'video'
STREAM_TYPE_AUDIO = 'audio'
STREAM_TYPE_SUBTITLE = 'subtitle'
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})'
SUBTITLE_FILE_EXTENSION = 'vtt'
def getModifiedStreamOrder(length, last):
"""This is jellyfin specific as the last stream in the order is set as default"""
seq = list(range(length))
if last < 0 or last > length -1:
return seq
seq.pop(last)
seq.append(last)
return seq
# def countStreamDispositions(subStreamDescriptor):
# return len([l for (k,v) in subStreamDescriptor['disposition'].items()])
def searchSubtitleFiles(dir, prefix):
sesl_match = re.compile(SEASON_EPISODE_STREAM_LANGUAGE_MATCH)
availableFileSubtitleDescriptors = []
for subtitleFilename in os.listdir(dir):
if subtitleFilename.startswith(prefix) and subtitleFilename.endswith('.' + SUBTITLE_FILE_EXTENSION):
sesl_result = sesl_match.search(subtitleFilename)
if sesl_result is not None:
subtitleFilePath = os.path.join(dir, subtitleFilename)
if os.path.isfile(subtitleFilePath):
subtitleFileDescriptor = {}
subtitleFileDescriptor['path'] = subtitleFilePath
subtitleFileDescriptor['season'] = int(sesl_result.group(1))
subtitleFileDescriptor['episode'] = int(sesl_result.group(2))
subtitleFileDescriptor['stream'] = int(sesl_result.group(3))
subtitleFileDescriptor['language'] = sesl_result.group(4)
availableFileSubtitleDescriptors.append(subtitleFileDescriptor)
click.echo(f"Found {len(availableFileSubtitleDescriptors)} subtitles in files\n")
return availableFileSubtitleDescriptors
@click.group() @click.group()
@click.pass_context @click.pass_context
@ -252,19 +203,16 @@ def convert(ctx,
# click.echo(f"Qualities: {q_list}") # click.echo(f"Qualities: {q_list}")
# #
# context['bitrates'] = {} context['bitrates'] = {}
# context['bitrates']['stereo'] = str(stereo_bitrate) if str(stereo_bitrate).endswith('k') else f"{stereo_bitrate}k" context['bitrates']['stereo'] = str(stereo_bitrate) if str(stereo_bitrate).endswith('k') else f"{stereo_bitrate}k"
# context['bitrates']['ac3'] = str(ac3_bitrate) if str(ac3_bitrate).endswith('k') else f"{ac3_bitrate}k" context['bitrates']['ac3'] = str(ac3_bitrate) if str(ac3_bitrate).endswith('k') else f"{ac3_bitrate}k"
# context['bitrates']['dts'] = str(dts_bitrate) if str(dts_bitrate).endswith('k') else f"{dts_bitrate}k" context['bitrates']['dts'] = str(dts_bitrate) if str(dts_bitrate).endswith('k') else f"{dts_bitrate}k"
# #
# click.echo(f"Stereo bitrate: {context['bitrates']['stereo']}") # click.echo(f"Stereo bitrate: {context['bitrates']['stereo']}")
# click.echo(f"AC3 bitrate: {context['bitrates']['ac3']}") # click.echo(f"AC3 bitrate: {context['bitrates']['ac3']}")
# click.echo(f"DTS bitrate: {context['bitrates']['dts']}") # click.echo(f"DTS bitrate: {context['bitrates']['dts']}")
# #
# #
# se_match = re.compile(SEASON_EPISODE_INDICATOR_MATCH)
# e_match = re.compile(EPISODE_INDICATOR_MATCH)
#
# #
# ## Conversion parameters # ## Conversion parameters
# #
@ -347,6 +295,10 @@ def convert(ctx,
dispositionTokens = fc.generateDispositionTokens() dispositionTokens = fc.generateDispositionTokens()
click.echo(f"Disposition Tokens: {dispositionTokens}") click.echo(f"Disposition Tokens: {dispositionTokens}")
audioTokens = fc.generateAudioEncodingTokens()
click.echo(f"Audio Tokens: {audioTokens}")
click.echo(f"Season={mediaFileProperties.getSeason()} Episode={mediaFileProperties.getEpisode()}")
# # Determine season and episode if present in current filename # # Determine season and episode if present in current filename
# season_digits = 2 # season_digits = 2

@ -5,7 +5,7 @@ from ffx.helper import DIFF_ADDED_KEY, DIFF_REMOVED_KEY, DIFF_CHANGED_KEY
from ffx.track_descriptor import TrackDescriptor from ffx.track_descriptor import TrackDescriptor
from ffx.model.track import Track from ffx.model.track import Track
from ffx.audio_layout import AudioLayout from ffx.audio_layout import AudioLayout
from ffx.track_type import TrackType
class FfxController(): class FfxController():
@ -104,41 +104,59 @@ class FfxController():
return ['-f', format, f"{filepath}.{ext}"] return ['-f', format, f"{filepath}.{ext}"]
def generateAudioEncodingTokens(self, subIndex : int, layout : AudioLayout): def generateAudioEncodingTokens(self):
"""Generates ffmpeg options for one output audio stream including channel remapping, codec and bitrate""" """Generates ffmpeg options audio streams including channel remapping, codec and bitrate"""
pass
audioTokens = []
if layout == AudioLayout.LAYOUT_6_1:
return [f"-c:a:{subIndex}", #sourceAudioTrackDescriptors = [smd for smd in self.__sourceMediaDescriptor.getAllTrackDescriptors() if smd.getType() == TrackType.AUDIO]
'libopus', targetAudioTrackDescriptors = [rtd for rtd in self.__targetMediaDescriptor.getReorderedTrackDescriptors() if rtd.getType() == TrackType.AUDIO]
f"-filter:a:{subIndex}",
'channelmap=channel_layout=6.1', trackSubIndex = 0
f"-b:a:{subIndex}", for trackDescriptor in targetAudioTrackDescriptors:
self.__context['bitrates']['dts']]
# Calculate source sub index
elif layout == AudioLayout.LAYOUT_5_1: #changedTargetTrackDescriptor : TrackDescriptor = targetAudioTrackDescriptors[trackDescriptor.getIndex()]
return [f"-c:a:{subIndex}", #changedTargetTrackSourceIndex = changedTargetTrackDescriptor.getSourceIndex()
'libopus', #sourceSubIndex = sourceAudioTrackDescriptors[changedTargetTrackSourceIndex].getSubIndex()
f"-filter:a:{subIndex}",
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1", trackAudioLayout = trackDescriptor.getAudioLayout()
f"-b:a:{subIndex}",
self.__context['bitrates']['ac3']] #TODO: Sollte nicht die sub index unverändert bleiben wenn jellyfin reordering angewendet wurde?
# siehe auch: MediaDescriptor.getInputMappingTokens()
elif layout == AudioLayout.LAYOUT_STEREO: #trackSubIndex = trackDescriptor.getSubIndex()
return [f"-c:a:{subIndex}",
'libopus', if trackAudioLayout == AudioLayout.LAYOUT_6_1:
f"-b:a:{subIndex}", audioTokens += [f"-c:a:{trackSubIndex}",
self.__context['bitrates']['stereo']] 'libopus',
f"-filter:a:{trackSubIndex}",
elif layout == AudioLayout.LAYOUT_6CH: 'channelmap=channel_layout=6.1',
return [f"-c:a:{subIndex}", f"-b:a:{trackSubIndex}",
'libopus', self.__context['bitrates']['dts']]
f"-filter:a:{subIndex}",
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1", if trackAudioLayout == AudioLayout.LAYOUT_5_1:
f"-b:a:{subIndex}", audioTokens += [f"-c:a:{trackSubIndex}",
self.__context['bitrates']['ac3']] 'libopus',
else: f"-filter:a:{trackSubIndex}",
return [] "channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
f"-b:a:{trackSubIndex}",
self.__context['bitrates']['ac3']]
if trackAudioLayout == AudioLayout.LAYOUT_STEREO:
audioTokens += [f"-c:a:{trackSubIndex}",
'libopus',
f"-b:a:{trackSubIndex}",
self.__context['bitrates']['stereo']]
if trackAudioLayout == AudioLayout.LAYOUT_6CH:
audioTokens += [f"-c:a:{trackSubIndex}",
'libopus',
f"-filter:a:{trackSubIndex}",
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
f"-b:a:{trackSubIndex}",
self.__context['bitrates']['ac3']]
trackSubIndex += 1
return audioTokens
# def generateClearTokens(self, streams): # def generateClearTokens(self, streams):

@ -16,6 +16,10 @@ class FileProperties():
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})'
SUBTITLE_FILE_EXTENSION = 'vtt'
def __init__(self, context, sourcePath): def __init__(self, context, sourcePath):
self.context = context self.context = context
@ -36,28 +40,26 @@ class FileProperties():
self.__sourceFilenameExtension = '' self.__sourceFilenameExtension = ''
se_match = re.compile(FileProperties.SEASON_EPISODE_INDICATOR_MATCH) self.__pc = PatternController(context)
e_match = re.compile(FileProperties.EPISODE_INDICATOR_MATCH)
se_result = se_match.search(self.__sourceFilename) matchResult = self.__pc.matchFilename(self.__sourceFilename)
e_result = e_match.search(self.__sourceFilename)
self.__season = -1 self.__pattern = matchResult['pattern'] if matchResult else None
self.__episode = -1
file_index = 0
if se_result is not None: matchedGroups = matchResult['match'].groups() if matchResult else {}
self.__season = int(se_result.group(1)) seIndicator = matchedGroups[0] if matchedGroups else self.__sourceFilename
self.__episode = int(se_result.group(2))
elif e_result is not None:
self.__episode = int(e_result.group(1))
else:
file_index += 1
se_match = re.search(FileProperties.SEASON_EPISODE_INDICATOR_MATCH, seIndicator)
e_match = re.search(FileProperties.EPISODE_INDICATOR_MATCH, seIndicator)
self.__pc = PatternController(context) self.__season = -1
self.__episode = -1
self.__pattern = self.__pc.matchFilename(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.__episode = int(e_match.group(1))
# click.echo(pattern) # click.echo(pattern)
@ -85,6 +87,33 @@ class FileProperties():
# #
def searchSubtitleFiles(dir, prefix):
sesl_match = re.compile(FileProperties.SEASON_EPISODE_STREAM_LANGUAGE_MATCH)
availableFileSubtitleDescriptors = []
for subtitleFilename in os.listdir(dir):
if subtitleFilename.startswith(prefix) and subtitleFilename.endswith('.' + FileProperties.SUBTITLE_FILE_EXTENSION):
sesl_result = sesl_match.search(subtitleFilename)
if sesl_result is not None:
subtitleFilePath = os.path.join(dir, subtitleFilename)
if os.path.isfile(subtitleFilePath):
subtitleFileDescriptor = {}
subtitleFileDescriptor['path'] = subtitleFilePath
subtitleFileDescriptor['season'] = int(sesl_result.group(1))
subtitleFileDescriptor['episode'] = int(sesl_result.group(2))
subtitleFileDescriptor['stream'] = int(sesl_result.group(3))
subtitleFileDescriptor['language'] = sesl_result.group(4)
availableFileSubtitleDescriptors.append(subtitleFileDescriptor)
click.echo(f"Found {len(availableFileSubtitleDescriptors)} subtitles in files\n")
return availableFileSubtitleDescriptors
def getFormatData(self): def getFormatData(self):
""" """
"format": { "format": {
@ -202,3 +231,11 @@ class FileProperties():
"""Result is None if the filename did not match anything in database""" """Result is None if the filename did not match anything in database"""
return self.__pattern return self.__pattern
def getSeason(self):
return int(self.__season)
def getEpisode(self):
return int(self.__episode)

@ -116,21 +116,24 @@ class PatternController():
s.close() s.close()
def matchFilename(self, filename) -> Pattern: def matchFilename(self, filename : str) -> re.Match:
try: try:
s = self.Session() s = self.Session()
q = s.query(Pattern) q = s.query(Pattern)
matchedPatterns = [p for p in q.all() if re.search(p.pattern, filename)] matchResult = {}
if matchedPatterns: for pattern in q.all():
return matchedPatterns[0] patternMatch = re.search(str(pattern.pattern), str(filename))
else: if patternMatch:
return None matchResult['match'] = patternMatch
matchResult['pattern'] = pattern
return matchResult
except Exception as ex: except Exception as ex:
raise click.ClickException(f"PatternController.findPattern(): {repr(ex)}") raise click.ClickException(f"PatternController.matchFilename(): {repr(ex)}")
finally: finally:
s.close() s.close()

Loading…
Cancel
Save