Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a -n/--name option to use a replacement name for the video #1161

Merged
merged 2 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading