Extd rename/unmux to pad with zeroes
This commit is contained in:
36
assets/ffx.json.j2
Normal file
36
assets/ffx.json.j2
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"databasePath": {{ database_path_json }},
|
||||||
|
"logDirectory": {{ log_directory_json }},
|
||||||
|
"subtitlesDirectory": {{ subtitles_directory_json }},
|
||||||
|
"defaultIndexSeasonDigits": {{ default_index_season_digits }},
|
||||||
|
"defaultIndexEpisodeDigits": {{ default_index_episode_digits }},
|
||||||
|
"defaultIndicatorSeasonDigits": {{ default_indicator_season_digits }},
|
||||||
|
"defaultIndicatorEpisodeDigits": {{ default_indicator_episode_digits }},
|
||||||
|
"metadata": {
|
||||||
|
"signature": {
|
||||||
|
"RECODED_WITH": "FFX"
|
||||||
|
},
|
||||||
|
"remove": [
|
||||||
|
"VERSION-eng",
|
||||||
|
"creation_time",
|
||||||
|
"NAME"
|
||||||
|
],
|
||||||
|
"streams": {
|
||||||
|
"remove": [
|
||||||
|
"BPS",
|
||||||
|
"NUMBER_OF_FRAMES",
|
||||||
|
"NUMBER_OF_BYTES",
|
||||||
|
"_STATISTICS_WRITING_APP",
|
||||||
|
"_STATISTICS_WRITING_DATE_UTC",
|
||||||
|
"_STATISTICS_TAGS",
|
||||||
|
"BPS-eng",
|
||||||
|
"DURATION-eng",
|
||||||
|
"NUMBER_OF_FRAMES-eng",
|
||||||
|
"NUMBER_OF_BYTES-eng",
|
||||||
|
"_STATISTICS_WRITING_APP-eng",
|
||||||
|
"_STATISTICS_WRITING_DATE_UTC-eng",
|
||||||
|
"_STATISTICS_TAGS-eng"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -185,8 +185,32 @@ def resolveUnmuxOutputDirectory(context, outputDirectory, subtitlesOnly, label):
|
|||||||
return os.path.join(configuredSubtitlesBaseDirectory, resolvedLabel), True
|
return os.path.join(configuredSubtitlesBaseDirectory, resolvedLabel), True
|
||||||
|
|
||||||
|
|
||||||
def buildRenameTargetFilename(sourcePath, prefix, seasonOverride=None, suffix=''):
|
def resolveIndicatorDigitLengths(context=None, showDescriptor=None):
|
||||||
|
from ffx.show_descriptor import ShowDescriptor
|
||||||
|
|
||||||
|
defaultDigitLengths = ShowDescriptor.getDefaultDigitLengths(context)
|
||||||
|
if showDescriptor is None:
|
||||||
|
return (
|
||||||
|
defaultDigitLengths[ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY],
|
||||||
|
defaultDigitLengths[ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
int(showDescriptor.getIndicatorSeasonDigits()),
|
||||||
|
int(showDescriptor.getIndicatorEpisodeDigits()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def buildRenameTargetFilename(
|
||||||
|
sourcePath,
|
||||||
|
prefix,
|
||||||
|
seasonOverride=None,
|
||||||
|
suffix='',
|
||||||
|
indicatorSeasonDigits=None,
|
||||||
|
indicatorEpisodeDigits=None,
|
||||||
|
):
|
||||||
from ffx.file_properties import FileProperties
|
from ffx.file_properties import FileProperties
|
||||||
|
from ffx.show_descriptor import ShowDescriptor
|
||||||
|
|
||||||
sourceFilename = os.path.basename(sourcePath)
|
sourceFilename = os.path.basename(sourcePath)
|
||||||
seasonEpisodeValues = FileProperties.extractSeasonEpisodeValues(sourceFilename)
|
seasonEpisodeValues = FileProperties.extractSeasonEpisodeValues(sourceFilename)
|
||||||
@@ -197,12 +221,22 @@ def buildRenameTargetFilename(sourcePath, prefix, seasonOverride=None, suffix=''
|
|||||||
resolvedSeason = int(seasonOverride) if seasonOverride is not None else (
|
resolvedSeason = int(seasonOverride) if seasonOverride is not None else (
|
||||||
int(sourceSeason) if sourceSeason is not None else 1
|
int(sourceSeason) if sourceSeason is not None else 1
|
||||||
)
|
)
|
||||||
|
resolvedIndicatorSeasonDigits = (
|
||||||
|
int(indicatorSeasonDigits)
|
||||||
|
if indicatorSeasonDigits is not None
|
||||||
|
else ShowDescriptor.DEFAULT_INDICATOR_SEASON_DIGITS
|
||||||
|
)
|
||||||
|
resolvedIndicatorEpisodeDigits = (
|
||||||
|
int(indicatorEpisodeDigits)
|
||||||
|
if indicatorEpisodeDigits is not None
|
||||||
|
else ShowDescriptor.DEFAULT_INDICATOR_EPISODE_DIGITS
|
||||||
|
)
|
||||||
|
|
||||||
_sourceBasename, sourceExtension = os.path.splitext(sourceFilename)
|
_sourceBasename, sourceExtension = os.path.splitext(sourceFilename)
|
||||||
|
|
||||||
targetFilenameTokens = [
|
targetFilenameTokens = [
|
||||||
str(prefix).strip(),
|
str(prefix).strip(),
|
||||||
f"s{resolvedSeason}e{int(sourceEpisode)}",
|
f"s{resolvedSeason:0{resolvedIndicatorSeasonDigits}d}e{int(sourceEpisode):0{resolvedIndicatorEpisodeDigits}d}",
|
||||||
]
|
]
|
||||||
|
|
||||||
resolvedSuffix = str(suffix).strip()
|
resolvedSuffix = str(suffix).strip()
|
||||||
@@ -444,10 +478,15 @@ def inspect(ctx, filename):
|
|||||||
@click.option('--dry-run', is_flag=True, default=False, help='Only print planned renames')
|
@click.option('--dry-run', is_flag=True, default=False, help='Only print planned renames')
|
||||||
def rename(ctx, paths, prefix, season, suffix, dry_run):
|
def rename(ctx, paths, prefix, season, suffix, dry_run):
|
||||||
"""Rename matching episode files in place."""
|
"""Rename matching episode files in place."""
|
||||||
|
from ffx.configuration_controller import ConfigurationController
|
||||||
|
|
||||||
resolvedPrefix = str(prefix).strip()
|
resolvedPrefix = str(prefix).strip()
|
||||||
resolvedSuffix = str(suffix).strip()
|
resolvedSuffix = str(suffix).strip()
|
||||||
effectiveDryRun = bool(ctx.obj.get('dry_run', False) or dry_run)
|
effectiveDryRun = bool(ctx.obj.get('dry_run', False) or dry_run)
|
||||||
|
renameContext = {
|
||||||
|
'config': ctx.obj.get('config') or ConfigurationController(),
|
||||||
|
}
|
||||||
|
indicatorSeasonDigits, indicatorEpisodeDigits = resolveIndicatorDigitLengths(renameContext)
|
||||||
|
|
||||||
if not resolvedPrefix:
|
if not resolvedPrefix:
|
||||||
raise click.ClickException("Rename prefix must not be empty.")
|
raise click.ClickException("Rename prefix must not be empty.")
|
||||||
@@ -463,6 +502,8 @@ def rename(ctx, paths, prefix, season, suffix, dry_run):
|
|||||||
resolvedPrefix,
|
resolvedPrefix,
|
||||||
seasonOverride=season,
|
seasonOverride=season,
|
||||||
suffix=resolvedSuffix,
|
suffix=resolvedSuffix,
|
||||||
|
indicatorSeasonDigits=indicatorSeasonDigits,
|
||||||
|
indicatorEpisodeDigits=indicatorEpisodeDigits,
|
||||||
)
|
)
|
||||||
if targetFilename is None:
|
if targetFilename is None:
|
||||||
continue
|
continue
|
||||||
@@ -571,13 +612,25 @@ def unmux(ctx,
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
sourceMediaDescriptor = fp.getMediaDescriptor()
|
sourceMediaDescriptor = fp.getMediaDescriptor()
|
||||||
|
currentPattern = fp.getPattern()
|
||||||
|
currentShowDescriptor = (
|
||||||
|
currentPattern.getShowDescriptor(ctx.obj) if currentPattern is not None else None
|
||||||
|
)
|
||||||
|
indicatorSeasonDigits, indicatorEpisodeDigits = resolveIndicatorDigitLengths(
|
||||||
|
ctx.obj,
|
||||||
|
currentShowDescriptor,
|
||||||
|
)
|
||||||
|
|
||||||
season = fp.getSeason()
|
season = fp.getSeason()
|
||||||
episode = fp.getEpisode()
|
episode = fp.getEpisode()
|
||||||
|
|
||||||
#TODO: Recognition für alle Formate anpassen
|
#TODO: Recognition für alle Formate anpassen
|
||||||
targetLabel = label if label else fp.getFileBasename()
|
targetLabel = label if label else fp.getFileBasename()
|
||||||
targetIndicator = f"_S{season}E{episode}" if label and season != -1 and episode != -1 else ''
|
targetIndicator = (
|
||||||
|
f"_S{season:0{indicatorSeasonDigits}d}E{episode:0{indicatorEpisodeDigits}d}"
|
||||||
|
if label and season != -1 and episode != -1
|
||||||
|
else ''
|
||||||
|
)
|
||||||
|
|
||||||
if label and not targetIndicator:
|
if label and not targetIndicator:
|
||||||
ctx.obj['logger'].warning(f"Skipping file {fp.getFilename()}: Label set but no indicator recognized")
|
ctx.obj['logger'].warning(f"Skipping file {fp.getFilename()}: Label set but no indicator recognized")
|
||||||
@@ -1226,10 +1279,11 @@ def convert(ctx,
|
|||||||
fc = FfxController(context, targetMediaDescriptor, sourceMediaDescriptor)
|
fc = FfxController(context, targetMediaDescriptor, sourceMediaDescriptor)
|
||||||
|
|
||||||
|
|
||||||
indexSeasonDigits = currentShowDescriptor.getIndexSeasonDigits() if not currentPattern is None else ShowDescriptor.DEFAULT_INDEX_SEASON_DIGITS
|
defaultDigitLengths = ShowDescriptor.getDefaultDigitLengths(context)
|
||||||
indexEpisodeDigits = currentShowDescriptor.getIndexEpisodeDigits() if not currentPattern is None else ShowDescriptor.DEFAULT_INDEX_EPISODE_DIGITS
|
indexSeasonDigits = currentShowDescriptor.getIndexSeasonDigits() if not currentPattern is None else defaultDigitLengths[ShowDescriptor.INDEX_SEASON_DIGITS_KEY]
|
||||||
indicatorSeasonDigits = currentShowDescriptor.getIndicatorSeasonDigits() if not currentPattern is None else ShowDescriptor.DEFAULT_INDICATOR_SEASON_DIGITS
|
indexEpisodeDigits = currentShowDescriptor.getIndexEpisodeDigits() if not currentPattern is None else defaultDigitLengths[ShowDescriptor.INDEX_EPISODE_DIGITS_KEY]
|
||||||
indicatorEpisodeDigits = currentShowDescriptor.getIndicatorEpisodeDigits() if not currentPattern is None else ShowDescriptor.DEFAULT_INDICATOR_EPISODE_DIGITS
|
indicatorSeasonDigits = currentShowDescriptor.getIndicatorSeasonDigits() if not currentPattern is None else defaultDigitLengths[ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY]
|
||||||
|
indicatorEpisodeDigits = currentShowDescriptor.getIndicatorEpisodeDigits() if not currentPattern is None else defaultDigitLengths[ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY]
|
||||||
|
|
||||||
|
|
||||||
# Shift season and episode if defined for this show
|
# Shift season and episode if defined for this show
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import os, json
|
import os, json
|
||||||
|
|
||||||
|
from .constants import (
|
||||||
|
DEFAULT_SHOW_INDEX_EPISODE_DIGITS,
|
||||||
|
DEFAULT_SHOW_INDEX_SEASON_DIGITS,
|
||||||
|
DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS,
|
||||||
|
DEFAULT_SHOW_INDICATOR_SEASON_DIGITS,
|
||||||
|
)
|
||||||
|
|
||||||
class ConfigurationController():
|
class ConfigurationController():
|
||||||
|
|
||||||
CONFIG_FILENAME = 'ffx.json'
|
CONFIG_FILENAME = 'ffx.json'
|
||||||
@@ -10,6 +17,10 @@ class ConfigurationController():
|
|||||||
LOG_DIRECTORY_CONFIG_KEY = 'logDirectory'
|
LOG_DIRECTORY_CONFIG_KEY = 'logDirectory'
|
||||||
SUBTITLES_DIRECTORY_CONFIG_KEY = 'subtitlesDirectory'
|
SUBTITLES_DIRECTORY_CONFIG_KEY = 'subtitlesDirectory'
|
||||||
OUTPUT_FILENAME_TEMPLATE_KEY = 'outputFilenameTemplate'
|
OUTPUT_FILENAME_TEMPLATE_KEY = 'outputFilenameTemplate'
|
||||||
|
DEFAULT_INDEX_SEASON_DIGITS_CONFIG_KEY = 'defaultIndexSeasonDigits'
|
||||||
|
DEFAULT_INDEX_EPISODE_DIGITS_CONFIG_KEY = 'defaultIndexEpisodeDigits'
|
||||||
|
DEFAULT_INDICATOR_SEASON_DIGITS_CONFIG_KEY = 'defaultIndicatorSeasonDigits'
|
||||||
|
DEFAULT_INDICATOR_EPISODE_DIGITS_CONFIG_KEY = 'defaultIndicatorEpisodeDigits'
|
||||||
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -57,6 +68,42 @@ class ConfigurationController():
|
|||||||
)
|
)
|
||||||
return os.path.expanduser(str(subtitlesDirectory)) if subtitlesDirectory else ''
|
return os.path.expanduser(str(subtitlesDirectory)) if subtitlesDirectory else ''
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getConfiguredIntegerValue(cls, configurationData: dict, configKey: str, defaultValue: int) -> int:
|
||||||
|
configuredValue = configurationData.get(configKey, defaultValue)
|
||||||
|
try:
|
||||||
|
return int(configuredValue)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return int(defaultValue)
|
||||||
|
|
||||||
|
def getDefaultIndexSeasonDigits(self):
|
||||||
|
return ConfigurationController.getConfiguredIntegerValue(
|
||||||
|
self.__configurationData,
|
||||||
|
ConfigurationController.DEFAULT_INDEX_SEASON_DIGITS_CONFIG_KEY,
|
||||||
|
DEFAULT_SHOW_INDEX_SEASON_DIGITS,
|
||||||
|
)
|
||||||
|
|
||||||
|
def getDefaultIndexEpisodeDigits(self):
|
||||||
|
return ConfigurationController.getConfiguredIntegerValue(
|
||||||
|
self.__configurationData,
|
||||||
|
ConfigurationController.DEFAULT_INDEX_EPISODE_DIGITS_CONFIG_KEY,
|
||||||
|
DEFAULT_SHOW_INDEX_EPISODE_DIGITS,
|
||||||
|
)
|
||||||
|
|
||||||
|
def getDefaultIndicatorSeasonDigits(self):
|
||||||
|
return ConfigurationController.getConfiguredIntegerValue(
|
||||||
|
self.__configurationData,
|
||||||
|
ConfigurationController.DEFAULT_INDICATOR_SEASON_DIGITS_CONFIG_KEY,
|
||||||
|
DEFAULT_SHOW_INDICATOR_SEASON_DIGITS,
|
||||||
|
)
|
||||||
|
|
||||||
|
def getDefaultIndicatorEpisodeDigits(self):
|
||||||
|
return ConfigurationController.getConfiguredIntegerValue(
|
||||||
|
self.__configurationData,
|
||||||
|
ConfigurationController.DEFAULT_INDICATOR_EPISODE_DIGITS_CONFIG_KEY,
|
||||||
|
DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS,
|
||||||
|
)
|
||||||
|
|
||||||
def getData(self):
|
def getData(self):
|
||||||
return self.__configurationData
|
return self.__configurationData
|
||||||
|
|
||||||
|
|||||||
@@ -22,4 +22,9 @@ DEFAULT_CROPDETECT_DURATION_SECONDS = 180
|
|||||||
DEFAULT_cut_start = 60
|
DEFAULT_cut_start = 60
|
||||||
DEFAULT_cut_length = 180
|
DEFAULT_cut_length = 180
|
||||||
|
|
||||||
|
DEFAULT_SHOW_INDEX_SEASON_DIGITS = 2
|
||||||
|
DEFAULT_SHOW_INDEX_EPISODE_DIGITS = 2
|
||||||
|
DEFAULT_SHOW_INDICATOR_SEASON_DIGITS = 2
|
||||||
|
DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS = 2
|
||||||
|
|
||||||
DEFAULT_OUTPUT_FILENAME_TEMPLATE = '{{ ffx_show_name }} - {{ ffx_index }}{{ ffx_index_separator }}{{ ffx_episode_name }}{{ ffx_indicator_separator }}{{ ffx_indicator }}'
|
DEFAULT_OUTPUT_FILENAME_TEMPLATE = '{{ ffx_show_name }} - {{ ffx_index }}{{ ffx_index_separator }}{{ ffx_episode_name }}{{ ffx_indicator_separator }}{{ ffx_indicator }}'
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from jinja2 import Environment, Undefined
|
|||||||
from .constants import DEFAULT_OUTPUT_FILENAME_TEMPLATE
|
from .constants import DEFAULT_OUTPUT_FILENAME_TEMPLATE
|
||||||
from .configuration_controller import ConfigurationController
|
from .configuration_controller import ConfigurationController
|
||||||
from .logging_utils import get_ffx_logger
|
from .logging_utils import get_ffx_logger
|
||||||
|
from .show_descriptor import ShowDescriptor
|
||||||
|
|
||||||
|
|
||||||
class EmptyStringUndefined(Undefined):
|
class EmptyStringUndefined(Undefined):
|
||||||
@@ -164,10 +165,10 @@ def getEpisodeFileBasename(showName,
|
|||||||
episodeName,
|
episodeName,
|
||||||
season,
|
season,
|
||||||
episode,
|
episode,
|
||||||
indexSeasonDigits = 2,
|
indexSeasonDigits = None,
|
||||||
indexEpisodeDigits = 2,
|
indexEpisodeDigits = None,
|
||||||
indicatorSeasonDigits = 2,
|
indicatorSeasonDigits = None,
|
||||||
indicatorEpisodeDigits = 2,
|
indicatorEpisodeDigits = None,
|
||||||
context = None):
|
context = None):
|
||||||
"""
|
"""
|
||||||
One Piece:
|
One Piece:
|
||||||
@@ -199,6 +200,16 @@ def getEpisodeFileBasename(showName,
|
|||||||
configData = cc.getData() if cc is not None else {}
|
configData = cc.getData() if cc is not None else {}
|
||||||
outputFilenameTemplate = configData.get(ConfigurationController.OUTPUT_FILENAME_TEMPLATE_KEY,
|
outputFilenameTemplate = configData.get(ConfigurationController.OUTPUT_FILENAME_TEMPLATE_KEY,
|
||||||
DEFAULT_OUTPUT_FILENAME_TEMPLATE)
|
DEFAULT_OUTPUT_FILENAME_TEMPLATE)
|
||||||
|
defaultDigitLengths = ShowDescriptor.getDefaultDigitLengths(context)
|
||||||
|
|
||||||
|
if indexSeasonDigits is None:
|
||||||
|
indexSeasonDigits = defaultDigitLengths[ShowDescriptor.INDEX_SEASON_DIGITS_KEY]
|
||||||
|
if indexEpisodeDigits is None:
|
||||||
|
indexEpisodeDigits = defaultDigitLengths[ShowDescriptor.INDEX_EPISODE_DIGITS_KEY]
|
||||||
|
if indicatorSeasonDigits is None:
|
||||||
|
indicatorSeasonDigits = defaultDigitLengths[ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY]
|
||||||
|
if indicatorEpisodeDigits is None:
|
||||||
|
indicatorEpisodeDigits = defaultDigitLengths[ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY]
|
||||||
|
|
||||||
if context is not None and 'logger' in context.keys():
|
if context is not None and 'logger' in context.keys():
|
||||||
logger = context['logger']
|
logger = context['logger']
|
||||||
|
|||||||
@@ -559,6 +559,7 @@ class MediaDetailsScreen(Screen):
|
|||||||
try:
|
try:
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
||||||
|
kwargs[ShowDescriptor.CONTEXT_KEY] = self.context
|
||||||
kwargs[ShowDescriptor.ID_KEY] = int(selected_row_data[0])
|
kwargs[ShowDescriptor.ID_KEY] = int(selected_row_data[0])
|
||||||
kwargs[ShowDescriptor.NAME_KEY] = str(selected_row_data[1])
|
kwargs[ShowDescriptor.NAME_KEY] = str(selected_row_data[1])
|
||||||
kwargs[ShowDescriptor.YEAR_KEY] = int(selected_row_data[2])
|
kwargs[ShowDescriptor.YEAR_KEY] = int(selected_row_data[2])
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
from .configuration_controller import ConfigurationController
|
||||||
|
from .constants import (
|
||||||
|
DEFAULT_SHOW_INDEX_EPISODE_DIGITS,
|
||||||
|
DEFAULT_SHOW_INDEX_SEASON_DIGITS,
|
||||||
|
DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS,
|
||||||
|
DEFAULT_SHOW_INDICATOR_SEASON_DIGITS,
|
||||||
|
)
|
||||||
from .logging_utils import get_ffx_logger
|
from .logging_utils import get_ffx_logger
|
||||||
|
|
||||||
|
|
||||||
@@ -15,10 +22,42 @@ class ShowDescriptor():
|
|||||||
INDICATOR_SEASON_DIGITS_KEY = 'indicator_season_digits'
|
INDICATOR_SEASON_DIGITS_KEY = 'indicator_season_digits'
|
||||||
INDICATOR_EPISODE_DIGITS_KEY = 'indicator_episode_digits'
|
INDICATOR_EPISODE_DIGITS_KEY = 'indicator_episode_digits'
|
||||||
|
|
||||||
DEFAULT_INDEX_SEASON_DIGITS = 2
|
DEFAULT_INDEX_SEASON_DIGITS = DEFAULT_SHOW_INDEX_SEASON_DIGITS
|
||||||
DEFAULT_INDEX_EPISODE_DIGITS = 2
|
DEFAULT_INDEX_EPISODE_DIGITS = DEFAULT_SHOW_INDEX_EPISODE_DIGITS
|
||||||
DEFAULT_INDICATOR_SEASON_DIGITS = 2
|
DEFAULT_INDICATOR_SEASON_DIGITS = DEFAULT_SHOW_INDICATOR_SEASON_DIGITS
|
||||||
DEFAULT_INDICATOR_EPISODE_DIGITS = 2
|
DEFAULT_INDICATOR_EPISODE_DIGITS = DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getDefaultDigitLengths(cls, context: dict | None = None) -> dict[str, int]:
|
||||||
|
configurationData = {}
|
||||||
|
|
||||||
|
if context is not None:
|
||||||
|
configController = context.get('config')
|
||||||
|
if configController is not None and hasattr(configController, 'getData'):
|
||||||
|
configurationData = configController.getData()
|
||||||
|
|
||||||
|
return {
|
||||||
|
cls.INDEX_SEASON_DIGITS_KEY: ConfigurationController.getConfiguredIntegerValue(
|
||||||
|
configurationData,
|
||||||
|
ConfigurationController.DEFAULT_INDEX_SEASON_DIGITS_CONFIG_KEY,
|
||||||
|
cls.DEFAULT_INDEX_SEASON_DIGITS,
|
||||||
|
),
|
||||||
|
cls.INDEX_EPISODE_DIGITS_KEY: ConfigurationController.getConfiguredIntegerValue(
|
||||||
|
configurationData,
|
||||||
|
ConfigurationController.DEFAULT_INDEX_EPISODE_DIGITS_CONFIG_KEY,
|
||||||
|
cls.DEFAULT_INDEX_EPISODE_DIGITS,
|
||||||
|
),
|
||||||
|
cls.INDICATOR_SEASON_DIGITS_KEY: ConfigurationController.getConfiguredIntegerValue(
|
||||||
|
configurationData,
|
||||||
|
ConfigurationController.DEFAULT_INDICATOR_SEASON_DIGITS_CONFIG_KEY,
|
||||||
|
cls.DEFAULT_INDICATOR_SEASON_DIGITS,
|
||||||
|
),
|
||||||
|
cls.INDICATOR_EPISODE_DIGITS_KEY: ConfigurationController.getConfiguredIntegerValue(
|
||||||
|
configurationData,
|
||||||
|
ConfigurationController.DEFAULT_INDICATOR_EPISODE_DIGITS_CONFIG_KEY,
|
||||||
|
cls.DEFAULT_INDICATOR_EPISODE_DIGITS,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@@ -55,34 +94,35 @@ class ShowDescriptor():
|
|||||||
else:
|
else:
|
||||||
self.__showYear = -1
|
self.__showYear = -1
|
||||||
|
|
||||||
|
defaultDigitLengths = self.getDefaultDigitLengths(self.__context)
|
||||||
|
|
||||||
if ShowDescriptor.INDEX_SEASON_DIGITS_KEY in kwargs.keys():
|
if ShowDescriptor.INDEX_SEASON_DIGITS_KEY in kwargs.keys():
|
||||||
if type(kwargs[ShowDescriptor.INDEX_SEASON_DIGITS_KEY]) is not int:
|
if type(kwargs[ShowDescriptor.INDEX_SEASON_DIGITS_KEY]) is not int:
|
||||||
raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDEX_SEASON_DIGITS_KEY} is required to be of type int")
|
raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDEX_SEASON_DIGITS_KEY} is required to be of type int")
|
||||||
self.__indexSeasonDigits = kwargs[ShowDescriptor.INDEX_SEASON_DIGITS_KEY]
|
self.__indexSeasonDigits = kwargs[ShowDescriptor.INDEX_SEASON_DIGITS_KEY]
|
||||||
else:
|
else:
|
||||||
self.__indexSeasonDigits = ShowDescriptor.DEFAULT_INDEX_SEASON_DIGITS
|
self.__indexSeasonDigits = defaultDigitLengths[ShowDescriptor.INDEX_SEASON_DIGITS_KEY]
|
||||||
|
|
||||||
if ShowDescriptor.INDEX_EPISODE_DIGITS_KEY in kwargs.keys():
|
if ShowDescriptor.INDEX_EPISODE_DIGITS_KEY in kwargs.keys():
|
||||||
if type(kwargs[ShowDescriptor.INDEX_EPISODE_DIGITS_KEY]) is not int:
|
if type(kwargs[ShowDescriptor.INDEX_EPISODE_DIGITS_KEY]) is not int:
|
||||||
raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDEX_EPISODE_DIGITS_KEY} is required to be of type int")
|
raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDEX_EPISODE_DIGITS_KEY} is required to be of type int")
|
||||||
self.__indexEpisodeDigits = kwargs[ShowDescriptor.INDEX_EPISODE_DIGITS_KEY]
|
self.__indexEpisodeDigits = kwargs[ShowDescriptor.INDEX_EPISODE_DIGITS_KEY]
|
||||||
else:
|
else:
|
||||||
self.__indexEpisodeDigits = ShowDescriptor.DEFAULT_INDEX_EPISODE_DIGITS
|
self.__indexEpisodeDigits = defaultDigitLengths[ShowDescriptor.INDEX_EPISODE_DIGITS_KEY]
|
||||||
|
|
||||||
if ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY in kwargs.keys():
|
if ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY in kwargs.keys():
|
||||||
if type(kwargs[ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY]) is not int:
|
if type(kwargs[ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY]) is not int:
|
||||||
raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY} is required to be of type int")
|
raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY} is required to be of type int")
|
||||||
self.__indicatorSeasonDigits = kwargs[ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY]
|
self.__indicatorSeasonDigits = kwargs[ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY]
|
||||||
else:
|
else:
|
||||||
self.__indicatorSeasonDigits = ShowDescriptor.DEFAULT_INDICATOR_SEASON_DIGITS
|
self.__indicatorSeasonDigits = defaultDigitLengths[ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY]
|
||||||
|
|
||||||
if ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY in kwargs.keys():
|
if ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY in kwargs.keys():
|
||||||
if type(kwargs[ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY]) is not int:
|
if type(kwargs[ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY]) is not int:
|
||||||
raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY} is required to be of type int")
|
raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY} is required to be of type int")
|
||||||
self.__indicatorEpisodeDigits = kwargs[ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY]
|
self.__indicatorEpisodeDigits = kwargs[ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY]
|
||||||
else:
|
else:
|
||||||
self.__indicatorEpisodeDigits = ShowDescriptor.DEFAULT_INDICATOR_EPISODE_DIGITS
|
self.__indicatorEpisodeDigits = defaultDigitLengths[ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY]
|
||||||
|
|
||||||
|
|
||||||
def getId(self):
|
def getId(self):
|
||||||
|
|||||||
@@ -160,11 +160,20 @@ class ShowDetailsScreen(Screen):
|
|||||||
self.updateShiftedSeasons()
|
self.updateShiftedSeasons()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
defaultDigitLengths = ShowDescriptor.getDefaultDigitLengths(self.context)
|
||||||
|
|
||||||
self.query_one("#index_season_digits_input", Input).value = "2"
|
self.query_one("#index_season_digits_input", Input).value = str(
|
||||||
self.query_one("#index_episode_digits_input", Input).value = "2"
|
defaultDigitLengths[ShowDescriptor.INDEX_SEASON_DIGITS_KEY]
|
||||||
self.query_one("#indicator_season_digits_input", Input).value = "2"
|
)
|
||||||
self.query_one("#indicator_episode_digits_input", Input).value = "2"
|
self.query_one("#index_episode_digits_input", Input).value = str(
|
||||||
|
defaultDigitLengths[ShowDescriptor.INDEX_EPISODE_DIGITS_KEY]
|
||||||
|
)
|
||||||
|
self.query_one("#indicator_season_digits_input", Input).value = str(
|
||||||
|
defaultDigitLengths[ShowDescriptor.INDICATOR_SEASON_DIGITS_KEY]
|
||||||
|
)
|
||||||
|
self.query_one("#indicator_episode_digits_input", Input).value = str(
|
||||||
|
defaultDigitLengths[ShowDescriptor.INDICATOR_EPISODE_DIGITS_KEY]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def getSelectedPatternDescriptor(self):
|
def getSelectedPatternDescriptor(self):
|
||||||
@@ -387,7 +396,7 @@ class ShowDetailsScreen(Screen):
|
|||||||
|
|
||||||
def getShowDescriptorFromInput(self) -> ShowDescriptor:
|
def getShowDescriptorFromInput(self) -> ShowDescriptor:
|
||||||
|
|
||||||
kwargs = {}
|
kwargs = {ShowDescriptor.CONTEXT_KEY: self.context}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.__showDescriptor:
|
if self.__showDescriptor:
|
||||||
|
|||||||
@@ -8,8 +8,18 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from tests.support.ffx_bundle import SourceTrackSpec, create_source_fixture
|
from tests.support.ffx_bundle import (
|
||||||
|
SourceTrackSpec,
|
||||||
|
build_controller_context,
|
||||||
|
create_source_fixture,
|
||||||
|
dispose_controller_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ffx.pattern_controller import PatternController
|
||||||
|
from ffx.show_controller import ShowController
|
||||||
|
from ffx.show_descriptor import ShowDescriptor
|
||||||
|
from ffx.track_codec import TrackCodec
|
||||||
|
from ffx.track_descriptor import TrackDescriptor
|
||||||
from ffx.track_type import TrackType
|
from ffx.track_type import TrackType
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -66,6 +76,39 @@ class UnmuxCliTests(unittest.TestCase):
|
|||||||
f"STDERR:\n{completed.stderr}"
|
f"STDERR:\n{completed.stderr}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def seed_matching_show(self, pattern_expression: str, *, indicator_season_digits: int, indicator_episode_digits: int) -> None:
|
||||||
|
context = build_controller_context(self.database_path)
|
||||||
|
try:
|
||||||
|
ShowController(context).updateShow(
|
||||||
|
ShowDescriptor(
|
||||||
|
id=1,
|
||||||
|
name="Unmux Test Show",
|
||||||
|
year=2000,
|
||||||
|
indicator_season_digits=indicator_season_digits,
|
||||||
|
indicator_episode_digits=indicator_episode_digits,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
PatternController(context).savePatternSchema(
|
||||||
|
{
|
||||||
|
"show_id": 1,
|
||||||
|
"pattern": pattern_expression,
|
||||||
|
"quality": 0,
|
||||||
|
"notes": "",
|
||||||
|
},
|
||||||
|
trackDescriptors=[
|
||||||
|
TrackDescriptor(
|
||||||
|
index=0,
|
||||||
|
source_index=0,
|
||||||
|
track_type=TrackType.VIDEO,
|
||||||
|
codec_name=TrackCodec.H264,
|
||||||
|
tags={},
|
||||||
|
disposition_set=set(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
dispose_controller_context(context)
|
||||||
|
|
||||||
def test_subtitles_only_without_output_directory_uses_configured_base_plus_label(self):
|
def test_subtitles_only_without_output_directory_uses_configured_base_plus_label(self):
|
||||||
self.write_config(
|
self.write_config(
|
||||||
{
|
{
|
||||||
@@ -101,6 +144,85 @@ class UnmuxCliTests(unittest.TestCase):
|
|||||||
expected_directory = self.home_dir / ".local" / "var" / "sync" / "subtitles" / "dball"
|
expected_directory = self.home_dir / ".local" / "var" / "sync" / "subtitles" / "dball"
|
||||||
self.assertTrue(expected_directory.is_dir(), expected_directory)
|
self.assertTrue(expected_directory.is_dir(), expected_directory)
|
||||||
|
|
||||||
|
def test_unmux_uses_configured_indicator_digits_in_output_filenames(self):
|
||||||
|
self.write_config(
|
||||||
|
{
|
||||||
|
"defaultIndicatorSeasonDigits": 3,
|
||||||
|
"defaultIndicatorEpisodeDigits": 4,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
source_filename = "unmux_s01e01.mkv"
|
||||||
|
output_directory = self.workdir / "unmux-output"
|
||||||
|
output_directory.mkdir()
|
||||||
|
source_path = create_source_fixture(
|
||||||
|
self.workdir,
|
||||||
|
source_filename,
|
||||||
|
[
|
||||||
|
SourceTrackSpec(TrackType.VIDEO, identity="video-0"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
completed = run_ffx_unmux(
|
||||||
|
self.workdir,
|
||||||
|
self.home_dir,
|
||||||
|
self.database_path,
|
||||||
|
"--label",
|
||||||
|
"dball",
|
||||||
|
"--output-directory",
|
||||||
|
str(output_directory),
|
||||||
|
str(source_path),
|
||||||
|
)
|
||||||
|
self.assertCompleted(completed)
|
||||||
|
|
||||||
|
output_filenames = sorted(path.name for path in output_directory.iterdir())
|
||||||
|
self.assertEqual(1, len(output_filenames), output_filenames)
|
||||||
|
self.assertTrue(
|
||||||
|
output_filenames[0].startswith("dball_S001E0001_"),
|
||||||
|
output_filenames,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unmux_prefers_matched_show_indicator_digits_over_config_defaults(self):
|
||||||
|
self.write_config(
|
||||||
|
{
|
||||||
|
"defaultIndicatorSeasonDigits": 4,
|
||||||
|
"defaultIndicatorEpisodeDigits": 4,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.seed_matching_show(
|
||||||
|
r"^unmux_([sS][0-9]+[eE][0-9]+)\.mkv$",
|
||||||
|
indicator_season_digits=1,
|
||||||
|
indicator_episode_digits=3,
|
||||||
|
)
|
||||||
|
source_filename = "unmux_s01e01.mkv"
|
||||||
|
output_directory = self.workdir / "unmux-output"
|
||||||
|
output_directory.mkdir()
|
||||||
|
source_path = create_source_fixture(
|
||||||
|
self.workdir,
|
||||||
|
source_filename,
|
||||||
|
[
|
||||||
|
SourceTrackSpec(TrackType.VIDEO, identity="video-0"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
completed = run_ffx_unmux(
|
||||||
|
self.workdir,
|
||||||
|
self.home_dir,
|
||||||
|
self.database_path,
|
||||||
|
"--label",
|
||||||
|
"dball",
|
||||||
|
"--output-directory",
|
||||||
|
str(output_directory),
|
||||||
|
str(source_path),
|
||||||
|
)
|
||||||
|
self.assertCompleted(completed)
|
||||||
|
|
||||||
|
output_filenames = sorted(path.name for path in output_directory.iterdir())
|
||||||
|
self.assertEqual(1, len(output_filenames), output_filenames)
|
||||||
|
self.assertTrue(
|
||||||
|
output_filenames[0].startswith("dball_S1E001_"),
|
||||||
|
output_filenames,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -21,6 +23,8 @@ class RenameCliTests(unittest.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.tempdir = tempfile.TemporaryDirectory()
|
self.tempdir = tempfile.TemporaryDirectory()
|
||||||
self.workspace = Path(self.tempdir.name)
|
self.workspace = Path(self.tempdir.name)
|
||||||
|
self.home_dir = self.workspace / "home"
|
||||||
|
self.home_dir.mkdir()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.tempdir.cleanup()
|
self.tempdir.cleanup()
|
||||||
@@ -30,9 +34,18 @@ class RenameCliTests(unittest.TestCase):
|
|||||||
source_path.write_bytes(payload)
|
source_path.write_bytes(payload)
|
||||||
return source_path
|
return source_path
|
||||||
|
|
||||||
|
def write_config(self, data: dict) -> None:
|
||||||
|
config_dir = self.home_dir / ".local" / "etc"
|
||||||
|
config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
(config_dir / "ffx.json").write_text(json.dumps(data), encoding="utf-8")
|
||||||
|
|
||||||
def invoke_rename(self, *args: str):
|
def invoke_rename(self, *args: str):
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(cli.ffx, ["rename", *args])
|
result = runner.invoke(
|
||||||
|
cli.ffx,
|
||||||
|
["rename", *args],
|
||||||
|
env={**os.environ, "HOME": str(self.home_dir)},
|
||||||
|
)
|
||||||
self.assertEqual(0, result.exit_code, result.output)
|
self.assertEqual(0, result.exit_code, result.output)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -41,8 +54,8 @@ class RenameCliTests(unittest.TestCase):
|
|||||||
|
|
||||||
result = self.invoke_rename("--prefix", "dball", str(source_path))
|
result = self.invoke_rename("--prefix", "dball", str(source_path))
|
||||||
|
|
||||||
target_path = self.workspace / "dball_s2e3.mkv"
|
target_path = self.workspace / "dball_s02e03.mkv"
|
||||||
self.assertIn("demo_S02E03.mkv -> dball_s2e3.mkv", result.output)
|
self.assertIn("demo_S02E03.mkv -> dball_s02e03.mkv", result.output)
|
||||||
self.assertFalse(source_path.exists())
|
self.assertFalse(source_path.exists())
|
||||||
self.assertTrue(target_path.exists())
|
self.assertTrue(target_path.exists())
|
||||||
self.assertEqual(b"season-episode", target_path.read_bytes())
|
self.assertEqual(b"season-episode", target_path.read_bytes())
|
||||||
@@ -58,8 +71,8 @@ class RenameCliTests(unittest.TestCase):
|
|||||||
str(source_path),
|
str(source_path),
|
||||||
)
|
)
|
||||||
|
|
||||||
target_path = self.workspace / "dball_s1e7_bonus.mp4"
|
target_path = self.workspace / "dball_s01e07_bonus.mp4"
|
||||||
self.assertIn("demo_E07.mp4 -> dball_s1e7_bonus.mp4", result.output)
|
self.assertIn("demo_E07.mp4 -> dball_s01e07_bonus.mp4", result.output)
|
||||||
self.assertFalse(source_path.exists())
|
self.assertFalse(source_path.exists())
|
||||||
self.assertTrue(target_path.exists())
|
self.assertTrue(target_path.exists())
|
||||||
self.assertEqual(b"episode-only", target_path.read_bytes())
|
self.assertEqual(b"episode-only", target_path.read_bytes())
|
||||||
@@ -75,8 +88,8 @@ class RenameCliTests(unittest.TestCase):
|
|||||||
str(source_path),
|
str(source_path),
|
||||||
)
|
)
|
||||||
|
|
||||||
target_path = self.workspace / "dball_s5e7.webm"
|
target_path = self.workspace / "dball_s05e07.webm"
|
||||||
self.assertIn("demo_s02e07.webm -> dball_s5e7.webm", result.output)
|
self.assertIn("demo_s02e07.webm -> dball_s05e07.webm", result.output)
|
||||||
self.assertFalse(source_path.exists())
|
self.assertFalse(source_path.exists())
|
||||||
self.assertTrue(target_path.exists())
|
self.assertTrue(target_path.exists())
|
||||||
|
|
||||||
@@ -90,11 +103,27 @@ class RenameCliTests(unittest.TestCase):
|
|||||||
str(source_path),
|
str(source_path),
|
||||||
)
|
)
|
||||||
|
|
||||||
target_path = self.workspace / "dball_s1e7.mkv"
|
target_path = self.workspace / "dball_s01e07.mkv"
|
||||||
self.assertIn("demo_E07.mkv -> dball_s1e7.mkv", result.output)
|
self.assertIn("demo_E07.mkv -> dball_s01e07.mkv", result.output)
|
||||||
self.assertTrue(source_path.exists())
|
self.assertTrue(source_path.exists())
|
||||||
self.assertFalse(target_path.exists())
|
self.assertFalse(target_path.exists())
|
||||||
|
|
||||||
|
def test_rename_uses_configured_indicator_digit_lengths(self):
|
||||||
|
self.write_config(
|
||||||
|
{
|
||||||
|
"defaultIndicatorSeasonDigits": 3,
|
||||||
|
"defaultIndicatorEpisodeDigits": 4,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
source_path = self.write_source("demo_E07.mkv")
|
||||||
|
|
||||||
|
result = self.invoke_rename("--prefix", "dball", str(source_path))
|
||||||
|
|
||||||
|
target_path = self.workspace / "dball_s001e0007.mkv"
|
||||||
|
self.assertIn("demo_E07.mkv -> dball_s001e0007.mkv", result.output)
|
||||||
|
self.assertFalse(source_path.exists())
|
||||||
|
self.assertTrue(target_path.exists())
|
||||||
|
|
||||||
def test_rename_skips_non_matching_filenames(self):
|
def test_rename_skips_non_matching_filenames(self):
|
||||||
source_path = self.write_source("demo_finale.mkv")
|
source_path = self.write_source("demo_finale.mkv")
|
||||||
|
|
||||||
|
|||||||
150
tests/unit/test_configure_workstation_script.py
Normal file
150
tests/unit/test_configure_workstation_script.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import textwrap
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
SCRIPT_PATH = REPO_ROOT / "tools" / "configure_workstation.sh"
|
||||||
|
BUNDLE_PYTHON = Path.home() / ".local" / "share" / "ffx.venv" / "bin" / "python"
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigureWorkstationScriptTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tempdir = tempfile.TemporaryDirectory()
|
||||||
|
self.home_dir = Path(self.tempdir.name) / "home"
|
||||||
|
self.home_dir.mkdir()
|
||||||
|
self.stub_bin_dir = Path(self.tempdir.name) / "bin"
|
||||||
|
self.stub_bin_dir.mkdir()
|
||||||
|
|
||||||
|
for command_name in ("git", "python3", "ffmpeg", "ffprobe", "cpulimit"):
|
||||||
|
self.write_stub_command(command_name)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.tempdir.cleanup()
|
||||||
|
|
||||||
|
def write_stub_command(self, name: str, body: str = "") -> None:
|
||||||
|
script_path = self.stub_bin_dir / name
|
||||||
|
script_path.write_text(
|
||||||
|
"#!/usr/bin/env bash\n"
|
||||||
|
+ body
|
||||||
|
+ "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
script_path.chmod(script_path.stat().st_mode | stat.S_IXUSR)
|
||||||
|
|
||||||
|
def run_script(self, **env_overrides: str) -> subprocess.CompletedProcess[str]:
|
||||||
|
if not BUNDLE_PYTHON.is_file():
|
||||||
|
self.skipTest(f"Missing bundle Python at {BUNDLE_PYTHON}")
|
||||||
|
|
||||||
|
env = {
|
||||||
|
**os.environ,
|
||||||
|
"HOME": str(self.home_dir),
|
||||||
|
"PATH": f"{self.stub_bin_dir}:{os.environ.get('PATH', '')}",
|
||||||
|
"FFX_PYTHON": str(BUNDLE_PYTHON),
|
||||||
|
**env_overrides,
|
||||||
|
}
|
||||||
|
|
||||||
|
return subprocess.run(
|
||||||
|
["bash", str(SCRIPT_PATH)],
|
||||||
|
capture_output=True,
|
||||||
|
cwd=REPO_ROOT,
|
||||||
|
env=env,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_script_seeds_default_config_from_template(self):
|
||||||
|
completed = self.run_script()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
0,
|
||||||
|
completed.returncode,
|
||||||
|
f"STDOUT:\n{completed.stdout}\nSTDERR:\n{completed.stderr}",
|
||||||
|
)
|
||||||
|
|
||||||
|
config_path = self.home_dir / ".local" / "etc" / "ffx.json"
|
||||||
|
self.assertTrue(config_path.exists())
|
||||||
|
|
||||||
|
config_data = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
"databasePath": str(self.home_dir / ".local" / "var" / "ffx" / "ffx.db"),
|
||||||
|
"logDirectory": str(self.home_dir / ".local" / "var" / "log"),
|
||||||
|
"subtitlesDirectory": str(
|
||||||
|
self.home_dir / ".local" / "var" / "sync" / "subtitles"
|
||||||
|
),
|
||||||
|
"defaultIndexSeasonDigits": 2,
|
||||||
|
"defaultIndexEpisodeDigits": 2,
|
||||||
|
"defaultIndicatorSeasonDigits": 2,
|
||||||
|
"defaultIndicatorEpisodeDigits": 2,
|
||||||
|
"metadata": {
|
||||||
|
"signature": {"RECODED_WITH": "FFX"},
|
||||||
|
"remove": [
|
||||||
|
"VERSION-eng",
|
||||||
|
"creation_time",
|
||||||
|
"NAME",
|
||||||
|
],
|
||||||
|
"streams": {
|
||||||
|
"remove": [
|
||||||
|
"BPS",
|
||||||
|
"NUMBER_OF_FRAMES",
|
||||||
|
"NUMBER_OF_BYTES",
|
||||||
|
"_STATISTICS_WRITING_APP",
|
||||||
|
"_STATISTICS_WRITING_DATE_UTC",
|
||||||
|
"_STATISTICS_TAGS",
|
||||||
|
"BPS-eng",
|
||||||
|
"DURATION-eng",
|
||||||
|
"NUMBER_OF_FRAMES-eng",
|
||||||
|
"NUMBER_OF_BYTES-eng",
|
||||||
|
"_STATISTICS_WRITING_APP-eng",
|
||||||
|
"_STATISTICS_WRITING_DATE_UTC-eng",
|
||||||
|
"_STATISTICS_TAGS-eng",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_script_honors_custom_template_override(self):
|
||||||
|
custom_template_path = Path(self.tempdir.name) / "custom-config.j2"
|
||||||
|
custom_template_path.write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"databasePath": {{ database_path_json }},
|
||||||
|
"marker": "from-template",
|
||||||
|
"subtitlesDirectory": {{ subtitles_directory_json }}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).lstrip(),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
completed = self.run_script(FFX_CONFIG_TEMPLATE=str(custom_template_path))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
0,
|
||||||
|
completed.returncode,
|
||||||
|
f"STDOUT:\n{completed.stdout}\nSTDERR:\n{completed.stderr}",
|
||||||
|
)
|
||||||
|
|
||||||
|
config_path = self.home_dir / ".local" / "etc" / "ffx.json"
|
||||||
|
config_data = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
self.assertEqual("from-template", config_data["marker"])
|
||||||
|
self.assertEqual(
|
||||||
|
str(self.home_dir / ".local" / "var" / "ffx" / "ffx.db"),
|
||||||
|
config_data["databasePath"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
97
tests/unit/test_show_descriptor_defaults.py
Normal file
97
tests/unit/test_show_descriptor_defaults.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
SRC_ROOT = Path(__file__).resolve().parents[2] / "src"
|
||||||
|
|
||||||
|
if str(SRC_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SRC_ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
from ffx.constants import (
|
||||||
|
DEFAULT_SHOW_INDEX_EPISODE_DIGITS,
|
||||||
|
DEFAULT_SHOW_INDEX_SEASON_DIGITS,
|
||||||
|
DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS,
|
||||||
|
DEFAULT_SHOW_INDICATOR_SEASON_DIGITS,
|
||||||
|
)
|
||||||
|
from ffx.helper import getEpisodeFileBasename
|
||||||
|
from ffx.show_descriptor import ShowDescriptor
|
||||||
|
|
||||||
|
|
||||||
|
class StaticConfig:
|
||||||
|
def __init__(self, data: dict | None = None):
|
||||||
|
self._data = data or {}
|
||||||
|
|
||||||
|
def getData(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
|
||||||
|
class ShowDescriptorDefaultTests(unittest.TestCase):
|
||||||
|
def make_context(self, config_data: dict | None = None) -> dict:
|
||||||
|
logger = logging.getLogger("ffx-test-show-descriptor-defaults")
|
||||||
|
logger.handlers = []
|
||||||
|
logger.addHandler(logging.NullHandler())
|
||||||
|
return {"config": StaticConfig(config_data), "logger": logger}
|
||||||
|
|
||||||
|
def test_show_descriptor_uses_config_defaults_when_context_is_present(self):
|
||||||
|
descriptor = ShowDescriptor(
|
||||||
|
context=self.make_context(
|
||||||
|
{
|
||||||
|
"defaultIndexSeasonDigits": "1",
|
||||||
|
"defaultIndexEpisodeDigits": "3",
|
||||||
|
"defaultIndicatorSeasonDigits": "3",
|
||||||
|
"defaultIndicatorEpisodeDigits": "4",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
id=1,
|
||||||
|
name="Configured Show",
|
||||||
|
year=2024,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(1, descriptor.getIndexSeasonDigits())
|
||||||
|
self.assertEqual(3, descriptor.getIndexEpisodeDigits())
|
||||||
|
self.assertEqual(3, descriptor.getIndicatorSeasonDigits())
|
||||||
|
self.assertEqual(4, descriptor.getIndicatorEpisodeDigits())
|
||||||
|
|
||||||
|
def test_show_descriptor_without_context_uses_shared_constants(self):
|
||||||
|
descriptor = ShowDescriptor(id=1, name="Default Show", year=2024)
|
||||||
|
|
||||||
|
self.assertEqual(DEFAULT_SHOW_INDEX_SEASON_DIGITS, descriptor.getIndexSeasonDigits())
|
||||||
|
self.assertEqual(DEFAULT_SHOW_INDEX_EPISODE_DIGITS, descriptor.getIndexEpisodeDigits())
|
||||||
|
self.assertEqual(
|
||||||
|
DEFAULT_SHOW_INDICATOR_SEASON_DIGITS,
|
||||||
|
descriptor.getIndicatorSeasonDigits(),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS,
|
||||||
|
descriptor.getIndicatorEpisodeDigits(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_episode_basename_uses_configured_digit_defaults_when_omitted(self):
|
||||||
|
basename = getEpisodeFileBasename(
|
||||||
|
"Configured Show",
|
||||||
|
"Episode Name",
|
||||||
|
2,
|
||||||
|
7,
|
||||||
|
context=self.make_context(
|
||||||
|
{
|
||||||
|
"defaultIndexSeasonDigits": 1,
|
||||||
|
"defaultIndexEpisodeDigits": 3,
|
||||||
|
"defaultIndicatorSeasonDigits": 3,
|
||||||
|
"defaultIndicatorEpisodeDigits": 4,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
"Configured Show - 2007 Episode Name - S002E0007",
|
||||||
|
basename,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
CONFIG_DIR="${FFX_CONFIG_DIR:-${HOME}/.local/etc}"
|
CONFIG_DIR="${FFX_CONFIG_DIR:-${HOME}/.local/etc}"
|
||||||
CONFIG_FILE="${FFX_CONFIG_FILE:-${CONFIG_DIR}/ffx.json}"
|
CONFIG_FILE="${FFX_CONFIG_FILE:-${CONFIG_DIR}/ffx.json}"
|
||||||
VAR_DIR="${FFX_VAR_DIR:-${HOME}/.local/var/ffx}"
|
VAR_DIR="${FFX_VAR_DIR:-${HOME}/.local/var/ffx}"
|
||||||
LOG_DIR="${FFX_LOG_DIR:-${HOME}/.local/var/log}"
|
LOG_DIR="${FFX_LOG_DIR:-${HOME}/.local/var/log}"
|
||||||
DATABASE_FILE="${FFX_DATABASE_FILE:-${VAR_DIR}/ffx.db}"
|
DATABASE_FILE="${FFX_DATABASE_FILE:-${VAR_DIR}/ffx.db}"
|
||||||
SUBTITLES_BASE_DIR="${FFX_SUBTITLES_BASE_DIR:-${HOME}/.local/var/sync/subtitles}"
|
SUBTITLES_BASE_DIR="${FFX_SUBTITLES_BASE_DIR:-${HOME}/.local/var/sync/subtitles}"
|
||||||
|
FFX_PYTHON="${FFX_PYTHON:-${HOME}/.local/share/ffx.venv/bin/python}"
|
||||||
|
CONFIG_TEMPLATE_FILE="${FFX_CONFIG_TEMPLATE:-${ROOT_DIR}/assets/ffx.json.j2}"
|
||||||
|
|
||||||
CHECK_ONLY=0
|
CHECK_ONLY=0
|
||||||
WITH_TESTS=0
|
WITH_TESTS=0
|
||||||
@@ -49,6 +52,8 @@ Environment overrides:
|
|||||||
FFX_LOG_DIR Override the default log directory.
|
FFX_LOG_DIR Override the default log directory.
|
||||||
FFX_DATABASE_FILE Override the database path written into a newly seeded config.
|
FFX_DATABASE_FILE Override the database path written into a newly seeded config.
|
||||||
FFX_SUBTITLES_BASE_DIR Override the default subtitles base directory written into a newly seeded config.
|
FFX_SUBTITLES_BASE_DIR Override the default subtitles base directory written into a newly seeded config.
|
||||||
|
FFX_PYTHON Override the bundle venv Python used to render the seeded config.
|
||||||
|
FFX_CONFIG_TEMPLATE Override the Jinja2 template path used to seed the config.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- tools/setup.sh is the first installation step and owns bundle venv setup.
|
- tools/setup.sh is the first installation step and owns bundle venv setup.
|
||||||
@@ -316,6 +321,93 @@ install_system_requirements() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render_default_config() {
|
||||||
|
local output_path="$1"
|
||||||
|
local temporary_output_path=""
|
||||||
|
|
||||||
|
if [ ! -x "${FFX_PYTHON}" ]; then
|
||||||
|
printf 'Missing bundle Python interpreter at %s.\n' "${FFX_PYTHON}" >&2
|
||||||
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "${CONFIG_TEMPLATE_FILE}" ]; then
|
||||||
|
printf 'Missing FFX config template at %s.\n' "${CONFIG_TEMPLATE_FILE}" >&2
|
||||||
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! temporary_output_path="$(mktemp "${output_path}.tmp.XXXXXX")"; then
|
||||||
|
printf 'Failed to create a temporary config file next to %s.\n' "${output_path}" >&2
|
||||||
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! FFX_CONFIG_TEMPLATE_FILE="${CONFIG_TEMPLATE_FILE}" \
|
||||||
|
FFX_REPO_ROOT="${ROOT_DIR}" \
|
||||||
|
FFX_DATABASE_PATH="${DATABASE_FILE}" \
|
||||||
|
FFX_LOG_DIRECTORY="${LOG_DIR}" \
|
||||||
|
FFX_SUBTITLES_DIRECTORY="${SUBTITLES_BASE_DIR}" \
|
||||||
|
"${FFX_PYTHON}" - >"${temporary_output_path}" <<'PY'
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader, StrictUndefined
|
||||||
|
|
||||||
|
repo_root = Path(os.environ["FFX_REPO_ROOT"])
|
||||||
|
src_root = repo_root / "src"
|
||||||
|
if str(src_root) not in sys.path:
|
||||||
|
sys.path.insert(0, str(src_root))
|
||||||
|
|
||||||
|
from ffx.constants import (
|
||||||
|
DEFAULT_SHOW_INDEX_EPISODE_DIGITS,
|
||||||
|
DEFAULT_SHOW_INDEX_SEASON_DIGITS,
|
||||||
|
DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS,
|
||||||
|
DEFAULT_SHOW_INDICATOR_SEASON_DIGITS,
|
||||||
|
)
|
||||||
|
|
||||||
|
template_path = Path(os.environ["FFX_CONFIG_TEMPLATE_FILE"])
|
||||||
|
environment = Environment(
|
||||||
|
loader=FileSystemLoader(str(template_path.parent)),
|
||||||
|
undefined=StrictUndefined,
|
||||||
|
autoescape=False,
|
||||||
|
keep_trailing_newline=True,
|
||||||
|
)
|
||||||
|
template = environment.get_template(template_path.name)
|
||||||
|
|
||||||
|
sys.stdout.write(
|
||||||
|
template.render(
|
||||||
|
database_path_json=json.dumps(os.environ["FFX_DATABASE_PATH"]),
|
||||||
|
log_directory_json=json.dumps(os.environ["FFX_LOG_DIRECTORY"]),
|
||||||
|
subtitles_directory_json=json.dumps(os.environ["FFX_SUBTITLES_DIRECTORY"]),
|
||||||
|
default_index_season_digits=DEFAULT_SHOW_INDEX_SEASON_DIGITS,
|
||||||
|
default_index_episode_digits=DEFAULT_SHOW_INDEX_EPISODE_DIGITS,
|
||||||
|
default_indicator_season_digits=DEFAULT_SHOW_INDICATOR_SEASON_DIGITS,
|
||||||
|
default_indicator_episode_digits=DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
PY
|
||||||
|
then
|
||||||
|
rm -f "${temporary_output_path}"
|
||||||
|
printf 'Failed to render ffx config from template %s.\n' "${CONFIG_TEMPLATE_FILE}" >&2
|
||||||
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! mv "${temporary_output_path}" "${output_path}"; then
|
||||||
|
rm -f "${temporary_output_path}"
|
||||||
|
printf 'Failed to move rendered ffx config into place at %s.\n' "${output_path}" >&2
|
||||||
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
seed_default_config() {
|
seed_default_config() {
|
||||||
if [ "${CHECK_ONLY}" -eq 1 ]; then
|
if [ "${CHECK_ONLY}" -eq 1 ]; then
|
||||||
return 0
|
return 0
|
||||||
@@ -365,43 +457,7 @@ seed_default_config() {
|
|||||||
|
|
||||||
if [ ! -f "${CONFIG_FILE}" ]; then
|
if [ ! -f "${CONFIG_FILE}" ]; then
|
||||||
printf 'Seeding ffx config at %s...\n' "${CONFIG_FILE}"
|
printf 'Seeding ffx config at %s...\n' "${CONFIG_FILE}"
|
||||||
if ! cat >"${CONFIG_FILE}" <<EOF
|
if ! render_default_config "${CONFIG_FILE}"; then
|
||||||
{
|
|
||||||
"databasePath": "${DATABASE_FILE}",
|
|
||||||
"logDirectory": "${LOG_DIR}",
|
|
||||||
"subtitlesDirectory": "${SUBTITLES_BASE_DIR}",
|
|
||||||
"metadata": {
|
|
||||||
"signature": {
|
|
||||||
"RECODED_WITH": "FFX"
|
|
||||||
},
|
|
||||||
"remove": [
|
|
||||||
"VERSION-eng",
|
|
||||||
"creation_time",
|
|
||||||
"NAME"
|
|
||||||
],
|
|
||||||
"streams": {
|
|
||||||
"remove": [
|
|
||||||
"BPS",
|
|
||||||
"NUMBER_OF_FRAMES",
|
|
||||||
"NUMBER_OF_BYTES",
|
|
||||||
"_STATISTICS_WRITING_APP",
|
|
||||||
"_STATISTICS_WRITING_DATE_UTC",
|
|
||||||
"_STATISTICS_TAGS",
|
|
||||||
"BPS-eng",
|
|
||||||
"DURATION-eng",
|
|
||||||
"NUMBER_OF_FRAMES-eng",
|
|
||||||
"NUMBER_OF_BYTES-eng",
|
|
||||||
"_STATISTICS_WRITING_APP-eng",
|
|
||||||
"_STATISTICS_WRITING_DATE_UTC-eng",
|
|
||||||
"_STATISTICS_TAGS-eng"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
then
|
|
||||||
printf 'Failed to write ffx config at %s.\n' "${CONFIG_FILE}" >&2
|
|
||||||
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
created_any=1
|
created_any=1
|
||||||
|
|||||||
Reference in New Issue
Block a user