season/episode recognition
This commit is contained in:
64
bin/ffx.py
64
bin/ffx.py
@@ -11,55 +11,6 @@ from ffx.database import databaseContext
|
||||
|
||||
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.pass_context
|
||||
@@ -252,19 +203,16 @@ def convert(ctx,
|
||||
|
||||
# click.echo(f"Qualities: {q_list}")
|
||||
#
|
||||
# context['bitrates'] = {}
|
||||
# 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']['dts'] = str(dts_bitrate) if str(dts_bitrate).endswith('k') else f"{dts_bitrate}k"
|
||||
context['bitrates'] = {}
|
||||
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']['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"AC3 bitrate: {context['bitrates']['ac3']}")
|
||||
# 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
|
||||
#
|
||||
@@ -347,6 +295,10 @@ def convert(ctx,
|
||||
dispositionTokens = fc.generateDispositionTokens()
|
||||
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
|
||||
# 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.model.track import Track
|
||||
from ffx.audio_layout import AudioLayout
|
||||
|
||||
from ffx.track_type import TrackType
|
||||
|
||||
class FfxController():
|
||||
|
||||
@@ -104,41 +104,59 @@ class FfxController():
|
||||
return ['-f', format, f"{filepath}.{ext}"]
|
||||
|
||||
|
||||
def generateAudioEncodingTokens(self, subIndex : int, layout : AudioLayout):
|
||||
"""Generates ffmpeg options for one output audio stream including channel remapping, codec and bitrate"""
|
||||
pass
|
||||
|
||||
if layout == AudioLayout.LAYOUT_6_1:
|
||||
return [f"-c:a:{subIndex}",
|
||||
'libopus',
|
||||
f"-filter:a:{subIndex}",
|
||||
'channelmap=channel_layout=6.1',
|
||||
f"-b:a:{subIndex}",
|
||||
self.__context['bitrates']['dts']]
|
||||
|
||||
elif layout == AudioLayout.LAYOUT_5_1:
|
||||
return [f"-c:a:{subIndex}",
|
||||
'libopus',
|
||||
f"-filter:a:{subIndex}",
|
||||
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
|
||||
f"-b:a:{subIndex}",
|
||||
self.__context['bitrates']['ac3']]
|
||||
|
||||
elif layout == AudioLayout.LAYOUT_STEREO:
|
||||
return [f"-c:a:{subIndex}",
|
||||
'libopus',
|
||||
f"-b:a:{subIndex}",
|
||||
self.__context['bitrates']['stereo']]
|
||||
|
||||
elif layout == AudioLayout.LAYOUT_6CH:
|
||||
return [f"-c:a:{subIndex}",
|
||||
'libopus',
|
||||
f"-filter:a:{subIndex}",
|
||||
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
|
||||
f"-b:a:{subIndex}",
|
||||
self.__context['bitrates']['ac3']]
|
||||
else:
|
||||
return []
|
||||
def generateAudioEncodingTokens(self):
|
||||
"""Generates ffmpeg options audio streams including channel remapping, codec and bitrate"""
|
||||
|
||||
audioTokens = []
|
||||
|
||||
#sourceAudioTrackDescriptors = [smd for smd in self.__sourceMediaDescriptor.getAllTrackDescriptors() if smd.getType() == TrackType.AUDIO]
|
||||
targetAudioTrackDescriptors = [rtd for rtd in self.__targetMediaDescriptor.getReorderedTrackDescriptors() if rtd.getType() == TrackType.AUDIO]
|
||||
|
||||
trackSubIndex = 0
|
||||
for trackDescriptor in targetAudioTrackDescriptors:
|
||||
|
||||
# Calculate source sub index
|
||||
#changedTargetTrackDescriptor : TrackDescriptor = targetAudioTrackDescriptors[trackDescriptor.getIndex()]
|
||||
#changedTargetTrackSourceIndex = changedTargetTrackDescriptor.getSourceIndex()
|
||||
#sourceSubIndex = sourceAudioTrackDescriptors[changedTargetTrackSourceIndex].getSubIndex()
|
||||
|
||||
trackAudioLayout = trackDescriptor.getAudioLayout()
|
||||
|
||||
#TODO: Sollte nicht die sub index unverändert bleiben wenn jellyfin reordering angewendet wurde?
|
||||
# siehe auch: MediaDescriptor.getInputMappingTokens()
|
||||
#trackSubIndex = trackDescriptor.getSubIndex()
|
||||
|
||||
if trackAudioLayout == AudioLayout.LAYOUT_6_1:
|
||||
audioTokens += [f"-c:a:{trackSubIndex}",
|
||||
'libopus',
|
||||
f"-filter:a:{trackSubIndex}",
|
||||
'channelmap=channel_layout=6.1',
|
||||
f"-b:a:{trackSubIndex}",
|
||||
self.__context['bitrates']['dts']]
|
||||
|
||||
if trackAudioLayout == AudioLayout.LAYOUT_5_1:
|
||||
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']]
|
||||
|
||||
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):
|
||||
|
||||
@@ -16,6 +16,10 @@ class FileProperties():
|
||||
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):
|
||||
|
||||
self.context = context
|
||||
@@ -36,28 +40,26 @@ class FileProperties():
|
||||
self.__sourceFilenameExtension = ''
|
||||
|
||||
|
||||
se_match = re.compile(FileProperties.SEASON_EPISODE_INDICATOR_MATCH)
|
||||
e_match = re.compile(FileProperties.EPISODE_INDICATOR_MATCH)
|
||||
self.__pc = PatternController(context)
|
||||
|
||||
se_result = se_match.search(self.__sourceFilename)
|
||||
e_result = e_match.search(self.__sourceFilename)
|
||||
matchResult = self.__pc.matchFilename(self.__sourceFilename)
|
||||
|
||||
self.__pattern = matchResult['pattern'] if matchResult else None
|
||||
|
||||
matchedGroups = matchResult['match'].groups() if matchResult else {}
|
||||
seIndicator = matchedGroups[0] if matchedGroups else self.__sourceFilename
|
||||
|
||||
se_match = re.search(FileProperties.SEASON_EPISODE_INDICATOR_MATCH, seIndicator)
|
||||
e_match = re.search(FileProperties.EPISODE_INDICATOR_MATCH, seIndicator)
|
||||
|
||||
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)
|
||||
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)
|
||||
@@ -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):
|
||||
"""
|
||||
"format": {
|
||||
@@ -201,4 +230,12 @@ class FileProperties():
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -116,21 +116,24 @@ class PatternController():
|
||||
s.close()
|
||||
|
||||
|
||||
def matchFilename(self, filename) -> Pattern:
|
||||
def matchFilename(self, filename : str) -> re.Match:
|
||||
|
||||
try:
|
||||
s = self.Session()
|
||||
q = s.query(Pattern)
|
||||
|
||||
matchedPatterns = [p for p in q.all() if re.search(p.pattern, filename)]
|
||||
|
||||
if matchedPatterns:
|
||||
return matchedPatterns[0]
|
||||
else:
|
||||
return None
|
||||
matchResult = {}
|
||||
|
||||
for pattern in q.all():
|
||||
patternMatch = re.search(str(pattern.pattern), str(filename))
|
||||
if patternMatch:
|
||||
matchResult['match'] = patternMatch
|
||||
matchResult['pattern'] = pattern
|
||||
|
||||
return matchResult
|
||||
|
||||
except Exception as ex:
|
||||
raise click.ClickException(f"PatternController.findPattern(): {repr(ex)}")
|
||||
raise click.ClickException(f"PatternController.matchFilename(): {repr(ex)}")
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user