Skip to content

Commit

Permalink
Add CLI and config file options to ignore refiner, provider and subti…
Browse files Browse the repository at this point in the history
…tle ids (#1160)

* add ignore refiner, provider and subtitle ids

* fix types

* remove tests
  • Loading branch information
getzze authored Sep 9, 2024
1 parent 608f68b commit dcbae82
Show file tree
Hide file tree
Showing 11 changed files with 447 additions and 87 deletions.
1 change: 1 addition & 0 deletions changelog.d/1018.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add CLI `ignore` option for refiners, providers and subtitle ids.
1 change: 1 addition & 0 deletions changelog.d/585.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add CLI `ignore` option for refiners, providers and subtitle ids.
3 changes: 2 additions & 1 deletion docs/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ apikey = "xxxxxxxxx"

[download]
provider = ["addic7ed", "opensubtitlescom", "opensubtitles"]
refiner = ["metadata", "hash", "omdb", "tmdb"]
refiner = ["metadata", "hash", "omdb"]
ignore_refiner = ["tmdb"]
language = ["fr", "en", "pt-br"]
encoding = "utf-8"
min_score = 50
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,12 @@ tvsubtitles = "subliminal.providers.tvsubtitles:TVsubtitlesProvider"
hash = "subliminal.refiners.hash:refine"
metadata = "subliminal.refiners.metadata:refine"
omdb = "subliminal.refiners.omdb:refine"
tmdb = "subliminal.refiners.tmdb:refine"
tvdb = "subliminal.refiners.tvdb:refine"

[project.entry-points."babelfish.language_converters"]
addic7ed = "subliminal.converters.addic7ed:Addic7edConverter"
opensubtitlescom = "subliminal.converters.opensubtitlescom:OpenSubtitlesComConverter"
shooter = "subliminal.converters.shooter:ShooterConverter"
thesubdb = "subliminal.converters.thesubdb:TheSubDBConverter"
tvsubtitles = "subliminal.converters.tvsubtitles:TVsubtitlesConverter"

[tool.setuptools]
Expand Down
121 changes: 110 additions & 11 deletions subliminal/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
scan_videos,
)
from subliminal.core import ARCHIVE_EXTENSIONS, scan_name, search_external_subtitles
from subliminal.extensions import default_providers, default_refiners
from subliminal.score import match_hearing_impaired
from subliminal.utils import merge_extend_and_ignore_unions

if TYPE_CHECKING:
from collections.abc import Sequence
Expand Down Expand Up @@ -125,11 +127,11 @@ def configure(ctx: click.Context, param: click.Parameter | None, filename: str |
with open(filename, 'rb') as f:
toml_dict = tomli.load(f)
except tomli.TOMLDecodeError:
msg = f'Cannot read the configuration file at {filename}'
msg = f'Cannot read the configuration file at "{filename}"'
else:
msg = f'Using configuration file at {filename}'
msg = f'Using configuration file at "{filename}"'
else:
msg = 'Not using any configuration file.'
msg = f'Not using any configuration file, not a file "{filename}"'

options = {}

Expand All @@ -140,14 +142,27 @@ def configure(ctx: click.Context, param: click.Parameter | None, filename: str |

# make download options
download_dict = toml_dict.setdefault('download', {})
# remove the provider and refiner lists to select, extend and ignore
provider_lists = {
'select': download_dict.pop('provider', []),
'extend': download_dict.pop('extend_provider', []),
'ignore': download_dict.pop('ignore_provider', []),
}
refiner_lists = {
'select': download_dict.pop('refiner', []),
'extend': download_dict.pop('extend_refiner', []),
'ignore': download_dict.pop('ignore_refiner', []),
}
options['download'] = download_dict

# make provider and refiner options
providers_dict = toml_dict.setdefault('provider', {})
refiners_dict = toml_dict.setdefault('refiner', {})

ctx.obj = {
'__config__': {'dict': toml_dict, 'debug_message': msg},
'debug_message': msg,
'provider_lists': provider_lists,
'refiner_lists': refiner_lists,
'provider_configs': providers_dict,
'refiner_configs': refiners_dict,
}
Expand All @@ -165,9 +180,9 @@ def plural(quantity: int, name: str, *, bold: bool = True, **kwargs: Any) -> str

AGE = AgeParamType()

PROVIDER = click.Choice(sorted(provider_manager.names()))
PROVIDER = click.Choice(['ALL', *sorted(provider_manager.names())])

REFINER = click.Choice(sorted(refiner_manager.names()))
REFINER = click.Choice(['ALL', *sorted(refiner_manager.names())])

dirs = PlatformDirs('subliminal')
cache_file = 'subliminal.dbm'
Expand Down Expand Up @@ -257,7 +272,7 @@ def subliminal(
logging.getLogger('subliminal').addHandler(handler)
logging.getLogger('subliminal').setLevel(logging.DEBUG)
# log about the config file
msg = ctx.obj['__config__']['debug_message']
msg = ctx.obj['debug_message']
logger.info(msg)

ctx.obj['debug'] = debug
Expand Down Expand Up @@ -305,7 +320,54 @@ def cache(ctx: click.Context, clear_subliminal: bool) -> None:
help='Language as IETF code, e.g. en, pt-BR (can be used multiple times).',
)
@click.option('-p', '--provider', type=PROVIDER, multiple=True, help='Provider to use (can be used multiple times).')
@click.option(
'-pp',
'--extend-provider',
type=PROVIDER,
multiple=True,
help=(
'Provider to use, on top of the default list (can be used multiple times). '
'Supersedes the providers used or ignored in the configuration file.'
),
)
@click.option(
'-P',
'--ignore-provider',
type=PROVIDER,
multiple=True,
help=(
'Provider to ignore (can be used multiple times). '
'Supersedes the providers used or ignored in the configuration file.'
),
)
@click.option('-r', '--refiner', type=REFINER, multiple=True, help='Refiner to use (can be used multiple times).')
@click.option(
'-rr',
'--extend-refiner',
type=REFINER,
multiple=True,
help=(
'Refiner to use, on top of the default list (can be used multiple times). '
'Supersedes the refiners used or ignored in the configuration file.'
),
)
@click.option(
'-R',
'--ignore-refiner',
type=REFINER,
multiple=True,
help=(
'Refiner to ignore (can be used multiple times). '
'Supersedes the refiners used or ignored in the configuration file.'
),
)
@click.option(
'-I',
'--ignore-subtitles',
type=click.STRING,
multiple=True,
help='Subtitle ids to ignore (can be used multiple times).',
)
@click.option('-a', '--age', type=AGE, help='Filter videos newer than AGE, e.g. 12h, 1w2d.')
@click.option(
'--use_creation_time',
Expand Down Expand Up @@ -381,7 +443,12 @@ def cache(ctx: click.Context, clear_subliminal: bool) -> None:
def download(
obj: dict[str, Any],
provider: Sequence[str],
extend_provider: Sequence[str],
ignore_provider: Sequence[str],
refiner: Sequence[str],
extend_refiner: Sequence[str],
ignore_refiner: Sequence[str],
ignore_subtitles: Sequence[str],
language: Sequence[Language],
age: timedelta | None,
use_ctime: bool,
Expand Down Expand Up @@ -420,6 +487,28 @@ def download(
if debug:
verbose = 3

# parse list of refiners
use_providers = merge_extend_and_ignore_unions(
{
'select': provider,
'extend': extend_provider,
'ignore': ignore_provider,
},
obj['provider_lists'],
default_providers,
)
logger.info('Use providers: %s', use_providers)
use_refiners = merge_extend_and_ignore_unions(
{
'select': refiner,
'extend': extend_refiner,
'ignore': ignore_refiner,
},
obj['refiner_lists'],
default_refiners,
)
logger.info('Use refiners: %s', use_refiners)

# scan videos
videos = []
ignored_videos = []
Expand Down Expand Up @@ -474,11 +563,10 @@ def download(
if check_video(video, languages=language_set, age=age, use_ctime=use_ctime, undefined=single):
refine(
video,
episode_refiners=refiner,
movie_refiners=refiner,
refiners=use_refiners,
refiner_configs=obj['refiner_configs'],
embedded_subtitles=not force,
providers=provider,
providers=use_providers,
languages=language_set,
)
videos.append(video)
Expand Down Expand Up @@ -516,11 +604,21 @@ def download(
if not videos:
return

# exit if no providers are used
if len(use_providers) == 0:
click.echo('No provider was selected to download subtitles.')
if 'ALL' in ignore_provider:
click.echo('All ignored from CLI argument: `--ignore-provider=ALL`')
elif 'ALL' in obj['provider_lists']['ignore']:
config_ignore = list(obj['provider_lists']['ignore'])
click.echo(f'All ignored from configuration: `ignore_provider={config_ignore}`')
return

# download best subtitles
downloaded_subtitles = defaultdict(list)
with AsyncProviderPool(
max_workers=max_workers,
providers=provider,
providers=use_providers,
provider_configs=obj['provider_configs'],
) as pp:
with click.progressbar(
Expand All @@ -540,6 +638,7 @@ def download(
min_score=scores['hash'] * min_score // 100,
hearing_impaired=hearing_impaired,
only_one=single,
ignore_subtitles=ignore_subtitles,
)
downloaded_subtitles[v] = subtitles

Expand Down
39 changes: 27 additions & 12 deletions subliminal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@
from guessit import guessit # type: ignore[import-untyped]
from rarfile import BadRarFile, Error, NotRarFile, RarCannotExec, RarFile, is_rarfile # type: ignore[import-untyped]

from .extensions import default_providers, provider_manager, refiner_manager
from .extensions import (
default_providers,
default_refiners,
discarded_episode_refiners,
discarded_movie_refiners,
provider_manager,
refiner_manager,
)
from .score import compute_score as default_compute_score
from .subtitle import SUBTITLE_EXTENSIONS, Subtitle
from .utils import get_age, handle_exception
Expand Down Expand Up @@ -65,7 +72,7 @@ def __init__(
providers: Sequence[str] | None = None,
provider_configs: Mapping[str, Any] | None = None,
) -> None:
self.providers = providers or default_providers
self.providers = providers if providers is not None else default_providers
self.provider_configs = provider_configs or {}
self.initialized_providers = {}
self.discarded_providers = set()
Expand Down Expand Up @@ -213,6 +220,7 @@ def download_best_subtitles(
hearing_impaired: bool = False,
only_one: bool = False,
compute_score: ComputeScore | None = None,
ignore_subtitles: Sequence[str] | None = None,
) -> list[Subtitle]:
"""Download the best matching subtitles.
Expand All @@ -227,11 +235,16 @@ def download_best_subtitles(
:param bool only_one: download only one subtitle, not one per language.
:param compute_score: function that takes `subtitle` and `video` as positional arguments,
`hearing_impaired` as keyword argument and returns the score.
:param ignore_subtitles: list of subtitle ids to ignore (None defaults to an empty list).
:return: downloaded subtitles.
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
"""
compute_score = compute_score or default_compute_score
ignore_subtitles = ignore_subtitles or []

# ignore subtitles
subtitles = [s for s in subtitles if s.id not in ignore_subtitles]

# sort subtitles by score
scored_subtitles = sorted(
Expand Down Expand Up @@ -303,7 +316,11 @@ def list_subtitles_provider_tuple(

def list_subtitles(self, video: Video, languages: Set[Language]) -> list[Subtitle]:
"""List subtitles, multi-threaded."""
subtitles = []
subtitles: list[Subtitle] = []

# Avoid raising a ValueError with `ThreadPoolExecutor(self.max_workers)`
if self.max_workers == 0:
return subtitles

with ThreadPoolExecutor(self.max_workers) as executor:
executor_map = executor.map(
Expand Down Expand Up @@ -648,8 +665,7 @@ def scan_videos(
def refine(
video: Video,
*,
episode_refiners: Sequence[str] | None = None,
movie_refiners: Sequence[str] | None = None,
refiners: Sequence[str] | None = None,
refiner_configs: Mapping[str, Any] | None = None,
**kwargs: Any,
) -> Video:
Expand All @@ -661,20 +677,19 @@ def refine(
:param video: the video to refine.
:type video: :class:`~subliminal.video.Video`
:param tuple episode_refiners: refiners to use for episodes.
:param tuple movie_refiners: refiners to use for movies.
:param Sequence refiners: refiners to select. None defaults to all refiners.
:param dict refiner_configs: refiner configuration as keyword arguments per refiner name to pass when
calling the refine method
:param kwargs: additional parameters for the :func:`~subliminal.refiners.refine` functions.
"""
refiners: tuple[str, ...] = ()
refiners = refiners if refiners is not None else default_refiners
if isinstance(video, Movie):
refiners = [r for r in refiners if r not in discarded_movie_refiners]
if isinstance(video, Episode):
refiners = tuple(episode_refiners) if episode_refiners is not None else ('metadata', 'tvdb', 'omdb', 'tmdb')
elif isinstance(video, Movie): # pragma: no branch
refiners = tuple(movie_refiners) if movie_refiners is not None else ('metadata', 'omdb', 'tmdb')
refiners = [r for r in refiners if r not in discarded_episode_refiners]

for refiner in ('hash', *refiners):
for refiner in refiners:
logger.info('Refining video with %s', refiner)
try:
refiner_manager[refiner].plugin(video, **dict((refiner_configs or {}).get(refiner, {}), **kwargs))
Expand Down
20 changes: 16 additions & 4 deletions subliminal/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def list_entry_points(self) -> list[EntryPoint]:
# registered extensions
for rep in self.registered_extensions:
ep = parse_entry_point(rep, self.namespace)
if ep.name not in [e.name for e in eps]:
if ep.name not in [e.name for e in eps]: # pragma: no branch
eps.append(ep)

return eps
Expand Down Expand Up @@ -84,7 +84,7 @@ def register(self, entry_point: str) -> None:
verify_requirements=False,
)
self.extensions.append(ext)
if self._extensions_by_name is not None:
if self._extensions_by_name is not None: # pragma: no branch
self._extensions_by_name[ext.name] = ext
self.registered_extensions.insert(0, entry_point)

Expand All @@ -101,9 +101,9 @@ def unregister(self, entry_point: str) -> None:

ep = parse_entry_point(entry_point, self.namespace)
self.registered_extensions.remove(entry_point)
if self._extensions_by_name is not None:
if self._extensions_by_name is not None: # pragma: no branch
del self._extensions_by_name[ep.name]
for i, ext in enumerate(self.extensions):
for i, ext in enumerate(self.extensions): # pragma: no branch
if ext.name == ep.name:
del self.extensions[i]
break
Expand Down Expand Up @@ -153,3 +153,15 @@ def parse_entry_point(src: str, group: str) -> EntryPoint:
'tmdb = subliminal.refiners.tmdb:refine',
],
)

#: Disabled refiners
disabled_refiners: list[str] = []

#: Default enabled refiners
default_refiners = [r for r in refiner_manager.names() if r not in disabled_refiners]

#: Discarded Movie refiners
discarded_movie_refiners: list[str] = ['tvdb']

#: Discarded Episode refiners
discarded_episode_refiners: list[str] = []
Loading

0 comments on commit dcbae82

Please sign in to comment.