Skip to content

Commit

Permalink
Add a -n/--name option to use a replacement name for the video (#1161)
Browse files Browse the repository at this point in the history
* sort files when scanning folder

* add an option to provide a replacement name for the video
  • Loading branch information
getzze authored Sep 9, 2024
1 parent 204e543 commit a0687ca
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 79 deletions.
2 changes: 2 additions & 0 deletions changelog.d/1132.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add a `-n/--name` option to use a replacement name for the video.
Sort files alphabetically before scanning a directory.
2 changes: 2 additions & 0 deletions changelog.d/991.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add a `-n/--name` option to use a replacement name for the video.
Sort files alphabetically before scanning a directory.
113 changes: 53 additions & 60 deletions subliminal/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
scan_video,
scan_videos,
)
from subliminal.core import ARCHIVE_EXTENSIONS, search_external_subtitles
from subliminal.core import ARCHIVE_EXTENSIONS, scan_name, search_external_subtitles
from subliminal.score import match_hearing_impaired

if TYPE_CHECKING:
Expand Down Expand Up @@ -355,6 +355,16 @@ def cache(ctx: click.Context, clear_subliminal: bool) -> None:
show_default=True,
help=f'Scan archives for videos (supported extensions: {", ".join(ARCHIVE_EXTENSIONS)}).',
)
@providers_config.option(
'-n',
'--name',
type=click.STRING,
metavar='NAME',
help=(
'Name used instead of the path name for guessing information about the file. '
'If used with multiple paths or a directory, `name` is passed to ALL the files.'
),
)
@click.option('-v', '--verbose', count=True, help='Increase verbosity.')
@click.argument('path', type=click.Path(), required=True, nargs=-1)
@click.pass_obj
Expand All @@ -373,6 +383,7 @@ def download(
min_score: int,
max_workers: int,
archives: bool,
name: str | None,
verbose: int,
path: list[str],
) -> None:
Expand Down Expand Up @@ -410,17 +421,45 @@ def download(
p = os.path.abspath(os.path.expanduser(p))
logger.debug('Collecting path %s', p)

video_candidates: list[Video] = []

# non-existing
if not os.path.exists(p):
try:
video = Video.fromname(p)
video = scan_name(p, name=name)
except ValueError:
repl_p = f'{p} ({name})' if name else p
logger.exception('Unexpected error while collecting non-existing path %s', repl_p)
errored_paths.append(p)
continue
video_candidates.append(video)

# directories
elif os.path.isdir(p):
try:
scanned_videos = scan_videos(p, age=age, archives=archives, name=name)
except ValueError:
repl_p = f'{p} ({name})' if name else p
logger.exception('Unexpected error while collecting directory path %s', repl_p)
errored_paths.append(p)
continue
video_candidates.extend(scanned_videos)

# other inputs
else:
try:
video = scan_video(p, name=name)
except ValueError:
logger.exception('Unexpected error while collecting non-existing path %s', p)
repl_p = f'{p} ({name})' if name else p
logger.exception('Unexpected error while collecting path %s', repl_p)
errored_paths.append(p)
continue
video_candidates.append(video)

# check and refine videos
for video in video_candidates:
if not force:
video.subtitles |= set(search_external_subtitles(video.name, directory=directory).values())

if check_video(video, languages=language_set, age=age, undefined=single):
refine(
video,
Expand All @@ -432,56 +471,8 @@ def download(
languages=language_set,
)
videos.append(video)
continue

# directories
if os.path.isdir(p):
try:
scanned_videos = scan_videos(p, age=age, archives=archives)
except ValueError:
logger.exception('Unexpected error while collecting directory path %s', p)
errored_paths.append(p)
continue
for video in scanned_videos:
if not force:
video.subtitles |= set(search_external_subtitles(video.name, directory=directory).values())
if check_video(video, languages=language_set, age=age, undefined=single):
refine(
video,
episode_refiners=refiner,
movie_refiners=refiner,
refiner_configs=obj['refiner_configs'],
embedded_subtitles=not force,
providers=provider,
languages=language_set,
)
videos.append(video)
else:
ignored_videos.append(video)
continue

# other inputs
try:
video = scan_video(p)
except ValueError:
logger.exception('Unexpected error while collecting path %s', p)
errored_paths.append(p)
continue
if not force:
video.subtitles |= set(search_external_subtitles(video.name, directory=directory).values())
if check_video(video, languages=language_set, age=age, undefined=single):
refine(
video,
episode_refiners=refiner,
movie_refiners=refiner,
refiner_configs=obj['refiner_configs'],
embedded_subtitles=not force,
providers=provider,
languages=language_set,
)
videos.append(video)
else:
ignored_videos.append(video)
else:
ignored_videos.append(video)

# output errored paths
if verbose > 0:
Expand All @@ -492,12 +483,14 @@ def download(
if verbose > 1:
for video in ignored_videos:
video_name = os.path.split(video.name)[1]
langs = ', '.join(str(s) for s in video.subtitle_languages) or 'none'
days = f"{video.age.days:d} day{'s' if video.age.days > 1 else ''}"
click.secho(
f'{video_name!r} ignored - subtitles: {langs} / age: {days}',
fg='yellow',
)
msg = f'{video_name!r} ignored'
if video.exists:
langs = ', '.join(str(s) for s in video.subtitle_languages) or 'none'
days = f"{video.age.days:d} day{'s' if video.age.days > 1 else ''}"
msg += f' - subtitles: {langs} / age: {days}'
else:
msg += ' - not a video file'
click.secho(msg, fg='yellow')

# report collected videos
click.echo(
Expand Down
56 changes: 42 additions & 14 deletions subliminal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def __delitem__(self, name: str) -> None:
try:
logger.info('Terminating provider %s', name)
self.initialized_providers[name].terminate()
except Exception as e: # noqa: BLE001
except Exception as e: # noqa: BLE001 # pragma: no cover
handle_exception(e, f'Provider {name} improperly terminated')

del self.initialized_providers[name]
Expand Down Expand Up @@ -190,7 +190,7 @@ def download_subtitle(self, subtitle: Subtitle) -> bool:
logger.info('Downloading subtitle %r', subtitle)
try:
self[subtitle.provider_name].download_subtitle(subtitle)
except (BadZipfile, BadRarFile):
except (BadZipfile, BadRarFile): # pragma: no cover
logger.exception('Bad archive for subtitle %r', subtitle)
except Exception as e: # noqa: BLE001
handle_exception(e, f'Discarding provider {subtitle.provider_name}')
Expand Down Expand Up @@ -424,10 +424,24 @@ def search_external_subtitles(
return subtitles


def scan_video(path: str | os.PathLike) -> Video:
def scan_name(path: str | os.PathLike, name: str | None = None) -> Video:
"""Scan a video from a `path` that does not exist.
:param str path: non-existing path to the video.
:param str name: if defined, name to use with guessit instead of the path.
:return: the scanned video.
:rtype: :class:`~subliminal.video.Video`
"""
path = os.fspath(path)
repl = name if name else path
return Video.fromguess(path, guessit(repl))


def scan_video(path: str | os.PathLike, name: str | None = None) -> Video:
"""Scan a video from a `path`.
:param str path: existing path to the video.
:param str name: if defined, name to use with guessit instead of the path.
:return: the scanned video.
:rtype: :class:`~subliminal.video.Video`
:raises: :class:`ValueError`: video path is not well defined.
Expand All @@ -444,10 +458,14 @@ def scan_video(path: str | os.PathLike) -> Video:
raise ValueError(msg)

dirpath, filename = os.path.split(path)
logger.info('Scanning video %r in %r', filename, dirpath)
repl = name if name else path
if name:
logger.info('Scanning video %r in %r, with replacement name %r', filename, dirpath, repl)
else:
logger.info('Scanning video %r in %r', filename, dirpath)

# guess
video = Video.fromguess(path, guessit(path))
video = Video.fromguess(path, guessit(repl))

# size
video.size = os.path.getsize(path)
Expand All @@ -456,17 +474,18 @@ def scan_video(path: str | os.PathLike) -> Video:
return video


def scan_archive(path: str | os.PathLike) -> Video:
def scan_archive(path: str | os.PathLike, name: str | None = None) -> Video:
"""Scan an archive from a `path`.
:param str path: existing path to the archive.
:param str name: if defined, name to use with guessit instead of the path.
:return: the scanned video.
:rtype: :class:`~subliminal.video.Video`
:raises: :class:`ValueError`: video path is not well defined.
"""
path = os.fspath(path)
# check for non-existing path
if not os.path.exists(path):
if not os.path.exists(path): # pragma: no cover
msg = 'Path does not exist'
raise ValueError(msg)

Expand Down Expand Up @@ -506,22 +525,31 @@ def scan_archive(path: str | os.PathLike) -> Video:
# guess
video_filename = file_info.filename
video_path = os.path.join(dir_path, video_filename)
video = Video.fromguess(video_path, guessit(video_path))

repl = name if name else video_path
video = Video.fromguess(video_path, guessit(repl))

# size
video.size = file_info.file_size

return video


def scan_videos(path: str | os.PathLike, *, age: timedelta | None = None, archives: bool = True) -> list[Video]:
def scan_videos(
path: str | os.PathLike,
*,
age: timedelta | None = None,
archives: bool = True,
name: str | None = None,
) -> list[Video]:
"""Scan `path` for videos and their subtitles.
See :func:`refine` to find additional information for the video.
:param str path: existing directory path to scan.
:param datetime.timedelta age: maximum age of the video or archive.
:param bool archives: scan videos in archives.
:param str name: name to use with guessit instead of the path.
:return: the scanned videos.
:rtype: list of :class:`~subliminal.video.Video`
:raises: :class:`ValueError`: video path is not well defined.
Expand Down Expand Up @@ -553,7 +581,7 @@ def scan_videos(path: str | os.PathLike, *, age: timedelta | None = None, archiv
dirnames.remove(dirname)

# scan for videos
for filename in filenames:
for filename in sorted(filenames):
# filter on videos and archives
if not filename.lower().endswith(VIDEO_EXTENSIONS) and not (
archives and filename.lower().endswith(ARCHIVE_EXTENSIONS)
Expand All @@ -580,7 +608,7 @@ def scan_videos(path: str | os.PathLike, *, age: timedelta | None = None, archiv
# skip old files
try:
file_age = datetime.fromtimestamp(os.path.getmtime(filepath), timezone.utc)
except ValueError:
except ValueError: # pragma: no cover
logger.warning('Could not get age of file %r in %r', filename, dirpath)
continue
else:
Expand All @@ -591,13 +619,13 @@ def scan_videos(path: str | os.PathLike, *, age: timedelta | None = None, archiv
# scan
if filename.lower().endswith(VIDEO_EXTENSIONS): # video
try:
video = scan_video(filepath)
video = scan_video(filepath, name=name)
except ValueError: # pragma: no cover
logger.exception('Error scanning video')
continue
elif archives and filename.lower().endswith(ARCHIVE_EXTENSIONS): # archive
try:
video = scan_archive(filepath)
video = scan_archive(filepath, name=name)
except (Error, NotRarFile, RarCannotExec, ValueError): # pragma: no cover
logger.exception('Error scanning archive')
continue
Expand Down Expand Up @@ -636,7 +664,7 @@ def refine(
refiners: tuple[str, ...] = ()
if isinstance(video, Episode):
refiners = tuple(episode_refiners) if episode_refiners is not None else ('metadata', 'tvdb', 'omdb', 'tmdb')
elif isinstance(video, Movie):
elif isinstance(video, Movie): # pragma: no branch
refiners = tuple(movie_refiners) if movie_refiners is not None else ('metadata', 'omdb', 'tmdb')

for refiner in ('hash', *refiners):
Expand Down
Loading

0 comments on commit a0687ca

Please sign in to comment.