Source code for flexget.plugins.output.subtitles_subliminal

import collections
import contextlib
import logging
import os
import tempfile

from loguru import logger

from flexget import plugin
from flexget.event import event

logger = logger.bind(name='subtitles')

try:
    from subliminal.extensions import provider_manager

    PROVIDERS = provider_manager.names()
except ImportError:
    PROVIDERS = [
        'addic7ed',
        'gestdown',
        'napiprojekt',
        'opensubtitles',
        'opensubtitlescom',
        'opensubtitlescomvip',
        'opensubtitlesvip',
        'podnapisi',
        'tvsubtitles',
    ]

AUTHENTICATION_SCHEMA = {provider: {'type': 'object'} for provider in PROVIDERS}


[docs] class PluginSubliminal: r"""Search and download subtitles using Subliminal by Antoine Bertin (https://pypi.python.org/pypi/subliminal). Example (complete task):: subs: find: path: - d:\media\incoming regexp: '.*\.(avi|mkv|mp4)$' recursive: yes accept_all: yes subliminal: languages: - ita alternatives: - eng exact_match: no providers: gestdown, opensubtitles single: no directory: /disk/subtitles hearing_impaired: yes authentication: opensubtitles: username: myuser password: mypassword """ schema = { 'type': 'object', 'properties': { 'languages': {'type': 'array', 'items': {'type': 'string'}, 'minItems': 1}, 'alternatives': {'type': 'array', 'items': {'type': 'string'}}, 'exact_match': {'type': 'boolean', 'default': True}, 'providers': {'type': 'array', 'items': {'type': 'string', 'enum': PROVIDERS}}, 'single': {'type': 'boolean', 'default': True}, 'directory': {'type': 'string'}, 'hearing_impaired': {'type': 'boolean', 'default': False}, 'authentication': {'type': 'object', 'properties': AUTHENTICATION_SCHEMA}, }, 'required': ['languages'], 'additionalProperties': False, }
[docs] def on_task_start(self, task, config): try: import babelfish # noqa: F401 except ImportError as e: logger.debug('Error importing Babelfish: {}', e) raise plugin.DependencyError( 'subliminal', 'babelfish', f'Babelfish module required. ImportError: {e}' ) try: import subliminal # noqa: F401 except ImportError as e: logger.debug('Error importing Subliminal: {}', e) raise plugin.DependencyError( 'subliminal', 'subliminal', f'Subliminal module required. ImportError: {e}' )
[docs] def on_task_output(self, task, config): """Register this as an output plugin. Configuration:: subliminal: languages: List of languages (as IETF codes) in order of preference. At least one is required. alternatives: List of second-choice languages; subs will be downloaded but entries rejected. exact_match: Use file hash only to search for subs, otherwise Subliminal will try to guess by filename. providers: List of providers from where to download subtitles. single: Download subtitles in single mode (no language code added to subtitle filename). directory: Path to directory where to save the subtitles, default is next to the video. hearing_impaired: Prefer subtitles for the hearing impaired when available authentication: > Dictionary of configuration options for different providers. Keys correspond to provider names, and values are dictionaries, usually specifying `username` and `password`. """ if not task.accepted: logger.debug('nothing accepted, aborting') return import subliminal from babelfish import Language from dogpile.cache.exception import RegionAlreadyConfigured from subliminal import save_subtitles, scan_video from subliminal.cli.helpers import MutexLock from subliminal.core import ( ARCHIVE_EXTENSIONS, refine, scan_archive, search_external_subtitles, ) from subliminal.score import episode_scores, movie_scores from subliminal.video import VIDEO_EXTENSIONS with contextlib.suppress(RegionAlreadyConfigured): subliminal.region.configure( 'dogpile.cache.dbm', arguments={ 'filename': os.path.join(tempfile.gettempdir(), 'cachefile.dbm'), 'lock_factory': MutexLock, }, ) # Let subliminal be more verbose if our logger is set to DEBUG if logger.level(task.manager.options.loglevel).no <= logger.level('DEBUG').no: logging.getLogger('subliminal').setLevel(logging.INFO) else: logging.getLogger('subliminal').setLevel(logging.CRITICAL) logging.getLogger('dogpile').setLevel(logging.CRITICAL) logging.getLogger('enzyme').setLevel(logging.WARNING) try: languages = {Language.fromietf(s) for s in config.get('languages', [])} alternative_languages = {Language.fromietf(s) for s in config.get('alternatives', [])} except ValueError as e: raise plugin.PluginError(e) # keep all downloaded subtitles and save to disk when done (no need to write every time) downloaded_subtitles = collections.defaultdict(list) providers_list = config.get('providers', None) provider_configs = config.get('authentication', None) # test if only one language was provided, if so we will download in single mode # (aka no language code added to subtitle filename) # unless we are forced not to by configuration # if we pass 'yes' for single in configuration but choose more than one language # we ignore the configuration and add the language code to the # potentially downloaded files single_mode = config.get('single', '') and len(languages | alternative_languages) <= 1 hearing_impaired = config.get('hearing_impaired', False) with subliminal.core.ProviderPool( providers=providers_list, provider_configs=provider_configs ) as provider_pool: for entry in task.accepted: if 'location' not in entry: logger.warning('Cannot act on entries that do not represent a local file.') continue if not entry['location'].exists(): entry.fail('file not found: {}'.format(entry['location'])) continue if '$RECYCLE.BIN' in str( entry['location'] ): # ignore deleted files in Windows shares continue try: entry_languages = set(entry.get('subtitle_languages', [])) or languages if entry['location'].suffix in VIDEO_EXTENSIONS: video = scan_video(entry['location']) elif entry['location'].suffix in ARCHIVE_EXTENSIONS: video = scan_archive(entry['location']) else: entry.reject( 'File extension is not a supported video or archive extension' ) continue # use metadata refiner to get mkv metadata refiner = ('metadata',) refine(video, episode_refiners=refiner, movie_refiners=refiner) video.subtitles.extend(search_external_subtitles(entry['location']).values()) if isinstance(video, subliminal.Episode): title = video.series hash_scores = episode_scores['hash'] else: title = video.title hash_scores = movie_scores['hash'] logger.info('Name computed for {} was {}', entry['location'], title) msc = hash_scores if config['exact_match'] else 0 if entry_languages.issubset(video.subtitle_languages): logger.debug( 'All preferred languages already exist for "{}"', entry['title'] ) entry['subtitles_missing'] = set() continue # subs for preferred lang(s) already exists # Gather the subtitles for the alternative languages too, to avoid needing to search the sites # again. They'll just be ignored if the main languages are found. all_subtitles = provider_pool.list_subtitles( video, entry_languages | alternative_languages ) try: subtitles = provider_pool.download_best_subtitles( all_subtitles, video, entry_languages, min_score=msc, hearing_impaired=hearing_impaired, ) except TypeError as e: logger.error( 'Downloading subtitles failed due to a bug in subliminal. Please seehttps://github.com/Diaoul/subliminal/issues/921. Error: {}', e, ) subtitles = [] if subtitles: downloaded_subtitles[video].extend(subtitles) logger.info('Subtitles found for {}', entry['location']) else: # only try to download for alternatives that aren't already downloaded subtitles = provider_pool.download_best_subtitles( all_subtitles, video, alternative_languages, min_score=msc, hearing_impaired=hearing_impaired, ) if subtitles: downloaded_subtitles[video].extend(subtitles) entry.reject('subtitles found for a second-choice language.') else: entry.reject('cannot find any subtitles for now.') downloaded_languages = {Language.fromietf(str(s.language)) for s in subtitles} if entry_languages: entry['subtitles_missing'] = entry_languages - downloaded_languages if len(entry['subtitles_missing']) > 0: entry.reject('Subtitles for all primary languages not found') except ValueError as e: logger.error('subliminal error: {}', e) entry.fail() if downloaded_subtitles: if task.options.test: logger.verbose('Test mode. Found subtitles:') # save subtitles to disk for video, subtitle in downloaded_subtitles.items(): if subtitle: _directory = config.get('directory') if _directory: _directory = os.path.expanduser(_directory) if task.options.test: logger.verbose( ' FOUND LANGUAGES {} for {}', [str(s.language) for s in subtitle], video.name, ) continue save_subtitles(video, subtitle, single=single_mode, directory=_directory)
[docs] @event('plugin.register') def register_plugin(): plugin.register(PluginSubliminal, 'subliminal', api_ver=2)