Adds rename command
This commit is contained in:
@@ -33,7 +33,7 @@ if TYPE_CHECKING:
|
||||
from ffx.media_descriptor import MediaDescriptor
|
||||
from ffx.track_descriptor import TrackDescriptor
|
||||
|
||||
LIGHTWEIGHT_COMMANDS = {None, 'version', 'help', 'setup', 'configure_workstation', 'upgrade'}
|
||||
LIGHTWEIGHT_COMMANDS = {None, 'version', 'help', 'setup', 'configure_workstation', 'upgrade', 'rename'}
|
||||
CPU_OPTION_HELP = (
|
||||
"Limit CPU for started processes. Use an absolute cpulimit value such as 200 "
|
||||
+ "(about 2 cores), or use a percentage such as 25% for a share of present cores. "
|
||||
@@ -185,6 +185,33 @@ def resolveUnmuxOutputDirectory(context, outputDirectory, subtitlesOnly, label):
|
||||
return os.path.join(configuredSubtitlesBaseDirectory, resolvedLabel), True
|
||||
|
||||
|
||||
def buildRenameTargetFilename(sourcePath, prefix, seasonOverride=None, suffix=''):
|
||||
from ffx.file_properties import FileProperties
|
||||
|
||||
sourceFilename = os.path.basename(sourcePath)
|
||||
seasonEpisodeValues = FileProperties.extractSeasonEpisodeValues(sourceFilename)
|
||||
if seasonEpisodeValues is None:
|
||||
return None
|
||||
|
||||
sourceSeason, sourceEpisode = seasonEpisodeValues
|
||||
resolvedSeason = int(seasonOverride) if seasonOverride is not None else (
|
||||
int(sourceSeason) if sourceSeason is not None else 1
|
||||
)
|
||||
|
||||
_sourceBasename, sourceExtension = os.path.splitext(sourceFilename)
|
||||
|
||||
targetFilenameTokens = [
|
||||
str(prefix).strip(),
|
||||
f"s{resolvedSeason}e{int(sourceEpisode)}",
|
||||
]
|
||||
|
||||
resolvedSuffix = str(suffix).strip()
|
||||
if resolvedSuffix:
|
||||
targetFilenameTokens.append(resolvedSuffix)
|
||||
|
||||
return f"{'_'.join(targetFilenameTokens)}{sourceExtension}"
|
||||
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.pass_context
|
||||
@@ -242,7 +269,7 @@ def version():
|
||||
def help():
|
||||
click.echo(f"ffx {VERSION}\n")
|
||||
click.echo("Maintenance commands: setup, configure_workstation, upgrade")
|
||||
click.echo("Media commands: shows, inspect, convert, unmux, cropdetect")
|
||||
click.echo("Media commands: shows, inspect, convert, rename, unmux, cropdetect")
|
||||
click.echo("Use 'ffx --help' or 'ffx <command> --help' for full command help.")
|
||||
|
||||
|
||||
@@ -408,6 +435,55 @@ def inspect(ctx, filename):
|
||||
app.run()
|
||||
|
||||
|
||||
@ffx.command()
|
||||
@click.pass_context
|
||||
@click.argument('paths', nargs=-1)
|
||||
@click.option('--prefix', type=str, required=True, help='Required target filename prefix')
|
||||
@click.option('--season', type=int, default=None, help='Override target season index')
|
||||
@click.option('--suffix', type=str, default='', help='Optional target filename suffix')
|
||||
@click.option('--dry-run', is_flag=True, default=False, help='Only print planned renames')
|
||||
def rename(ctx, paths, prefix, season, suffix, dry_run):
|
||||
"""Rename matching episode files in place."""
|
||||
|
||||
resolvedPrefix = str(prefix).strip()
|
||||
resolvedSuffix = str(suffix).strip()
|
||||
effectiveDryRun = bool(ctx.obj.get('dry_run', False) or dry_run)
|
||||
|
||||
if not resolvedPrefix:
|
||||
raise click.ClickException("Rename prefix must not be empty.")
|
||||
|
||||
processedCount = 0
|
||||
|
||||
for sourcePath in paths:
|
||||
if not os.path.isfile(sourcePath):
|
||||
continue
|
||||
|
||||
targetFilename = buildRenameTargetFilename(
|
||||
sourcePath,
|
||||
resolvedPrefix,
|
||||
seasonOverride=season,
|
||||
suffix=resolvedSuffix,
|
||||
)
|
||||
if targetFilename is None:
|
||||
continue
|
||||
|
||||
sourceFilename = os.path.basename(sourcePath)
|
||||
targetPath = os.path.join(os.path.dirname(sourcePath), targetFilename)
|
||||
click.echo(f"{sourceFilename} -> {targetFilename}")
|
||||
processedCount += 1
|
||||
|
||||
if effectiveDryRun or os.path.abspath(sourcePath) == os.path.abspath(targetPath):
|
||||
continue
|
||||
|
||||
if os.path.exists(targetPath):
|
||||
raise click.ClickException(f"Target file already exists: {targetPath}")
|
||||
|
||||
shutil.move(sourcePath, targetPath)
|
||||
|
||||
if processedCount == 0:
|
||||
click.echo("No matching files found.")
|
||||
|
||||
|
||||
def getUnmuxSequence(trackDescriptor: TrackDescriptor, sourcePath, targetPrefix, targetDirectory = ''):
|
||||
|
||||
# executable and input file
|
||||
|
||||
@@ -30,6 +30,18 @@ class FileProperties():
|
||||
|
||||
DEFAULT_INDEX_DIGITS = 3
|
||||
|
||||
@classmethod
|
||||
def extractSeasonEpisodeValues(cls, sourceText: str) -> tuple[int | None, int] | None:
|
||||
seasonEpisodeMatch = re.search(cls.SEASON_EPISODE_INDICATOR_MATCH, str(sourceText))
|
||||
if seasonEpisodeMatch is not None:
|
||||
return int(seasonEpisodeMatch.group(1)), int(seasonEpisodeMatch.group(2))
|
||||
|
||||
episodeMatch = re.search(cls.EPISODE_INDICATOR_MATCH, str(sourceText))
|
||||
if episodeMatch is not None:
|
||||
return None, int(episodeMatch.group(1))
|
||||
|
||||
return None
|
||||
|
||||
def __init__(self, context, sourcePath):
|
||||
|
||||
self.context = context
|
||||
@@ -65,26 +77,19 @@ class FileProperties():
|
||||
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)
|
||||
|
||||
indicatorSource = databaseMatchedGroups[0]
|
||||
else:
|
||||
self.__logger.debug(f"FileProperties.__init__(): Checking file name for indicator {self.__sourceFilename}")
|
||||
indicatorSource = 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:
|
||||
seasonEpisodeValues = self.extractSeasonEpisodeValues(indicatorSource)
|
||||
if seasonEpisodeValues is None:
|
||||
self.__season = -1
|
||||
self.__episode = -1
|
||||
else:
|
||||
sourceSeason, sourceEpisode = seasonEpisodeValues
|
||||
self.__season = -1 if sourceSeason is None else int(sourceSeason)
|
||||
self.__episode = int(sourceEpisode)
|
||||
|
||||
self.__ffprobeData = None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user