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'
|
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
|
|
||||||
|
|
||||||
if layout == AudioLayout.LAYOUT_6_1:
|
audioTokens = []
|
||||||
return [f"-c:a:{subIndex}",
|
|
||||||
|
#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',
|
'libopus',
|
||||||
f"-filter:a:{subIndex}",
|
f"-filter:a:{trackSubIndex}",
|
||||||
'channelmap=channel_layout=6.1',
|
'channelmap=channel_layout=6.1',
|
||||||
f"-b:a:{subIndex}",
|
f"-b:a:{trackSubIndex}",
|
||||||
self.__context['bitrates']['dts']]
|
self.__context['bitrates']['dts']]
|
||||||
|
|
||||||
elif layout == AudioLayout.LAYOUT_5_1:
|
if trackAudioLayout == AudioLayout.LAYOUT_5_1:
|
||||||
return [f"-c:a:{subIndex}",
|
audioTokens += [f"-c:a:{trackSubIndex}",
|
||||||
'libopus',
|
'libopus',
|
||||||
f"-filter:a:{subIndex}",
|
f"-filter:a:{trackSubIndex}",
|
||||||
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
|
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
|
||||||
f"-b:a:{subIndex}",
|
f"-b:a:{trackSubIndex}",
|
||||||
self.__context['bitrates']['ac3']]
|
self.__context['bitrates']['ac3']]
|
||||||
|
|
||||||
elif layout == AudioLayout.LAYOUT_STEREO:
|
if trackAudioLayout == AudioLayout.LAYOUT_STEREO:
|
||||||
return [f"-c:a:{subIndex}",
|
audioTokens += [f"-c:a:{trackSubIndex}",
|
||||||
'libopus',
|
'libopus',
|
||||||
f"-b:a:{subIndex}",
|
f"-b:a:{trackSubIndex}",
|
||||||
self.__context['bitrates']['stereo']]
|
self.__context['bitrates']['stereo']]
|
||||||
|
|
||||||
elif layout == AudioLayout.LAYOUT_6CH:
|
if trackAudioLayout == AudioLayout.LAYOUT_6CH:
|
||||||
return [f"-c:a:{subIndex}",
|
audioTokens += [f"-c:a:{trackSubIndex}",
|
||||||
'libopus',
|
'libopus',
|
||||||
f"-filter:a:{subIndex}",
|
f"-filter:a:{trackSubIndex}",
|
||||||
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
|
"channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1",
|
||||||
f"-b:a:{subIndex}",
|
f"-b:a:{trackSubIndex}",
|
||||||
self.__context['bitrates']['ac3']]
|
self.__context['bitrates']['ac3']]
|
||||||
else:
|
trackSubIndex += 1
|
||||||
return []
|
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.__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.__season = -1
|
||||||
self.__episode = -1
|
self.__episode = -1
|
||||||
file_index = 0
|
|
||||||
|
|
||||||
if se_result is not None:
|
if se_match is not None:
|
||||||
self.__season = int(se_result.group(1))
|
self.__season = int(se_match.group(1))
|
||||||
self.__episode = int(se_result.group(2))
|
self.__episode = int(se_match.group(2))
|
||||||
elif e_result is not None:
|
elif e_match is not None:
|
||||||
self.__episode = int(e_result.group(1))
|
self.__episode = int(e_match.group(1))
|
||||||
else:
|
|
||||||
file_index += 1
|
|
||||||
|
|
||||||
|
|
||||||
self.__pc = PatternController(context)
|
|
||||||
|
|
||||||
self.__pattern = self.__pc.matchFilename(self.__sourceFilename)
|
|
||||||
|
|
||||||
|
|
||||||
# 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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user