Files

309 lines
13 KiB
Python

# coding=utf-8
import logging
import traceback
import requests
import socket
import operator
import time
from babelfish.exceptions import LanguageReverseError
from pkg_resources import EntryPoint, iter_entry_points
from subliminal import ProviderError
from subliminal.api import ProviderPool
from subliminal_patch.patch_subtitle import compute_score
logger = logging.getLogger(__name__)
DOWNLOAD_TRIES = 0
DOWNLOAD_RETRY_SLEEP = 2
class OldToNewProvider(object):
"""
Simple proxy class to support the .plugin property which would normally exist
when this was a stevedore.extension
"""
def __init__(self, provider):
self.provider = provider
def plugin(self):
return self.provider
plugin = property(plugin)
class LegacyProviderManager(object):
"""
This is the old ProviderManager subliminal used in its pre-1.0 versions, not relying on stevedore.
Its providers are wrapped inside OldToNewProvider instances, which support the .plugin property
subliminal expects.
Old Doc: Manager for providers behaving like a dict with lazy loading
Loading is done in this order:
* Entry point providers
* Registered providers
.. attribute:: entry_point
The entry point where to look for providers
"""
entry_point = 'subliminal.providers'
def __init__(self, enabled_providers=None):
#: Registered providers with entry point syntax
self.registered_providers = ['addic7ed = subliminal.providers.addic7ed:Addic7edProvider',
'opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider',
'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider',
'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider',
'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider']
self.enabled_providers = enabled_providers or []
#: Loaded providers
self.providers = {}
@property
def available_providers(self):
"""Available providers"""
available_providers = set(self.providers.keys())
available_providers.update([ep.name for ep in iter_entry_points(self.entry_point)])
available_providers.update([EntryPoint.parse(c).name for c in self.registered_providers])
return available_providers
def __getitem__(self, name):
"""Get a provider, lazy loading it if necessary"""
if name in self.enabled_providers and name in self.providers:
return self.providers[name]
for ep in iter_entry_points(self.entry_point):
if ep.name == name and name in self.enabled_providers:
self.providers[ep.name] = OldToNewProvider(ep.load())
return self.providers[ep.name]
for ep in (EntryPoint.parse(c) for c in self.registered_providers):
if ep.name == name and name in self.enabled_providers:
self.providers[ep.name] = OldToNewProvider(ep.load(require=False))
return self.providers[ep.name]
raise KeyError(name)
def __setitem__(self, name, provider):
"""Load a provider"""
self.providers[name] = provider
def __delitem__(self, name):
"""Unload a provider"""
del self.providers[name]
def __iter__(self):
"""Iterator over loaded providers"""
return iter(self.providers)
def register(self, entry_point):
"""Register a provider
:param string entry_point: provider to register (entry point syntax)
:raise: ValueError if already registered
"""
if entry_point in self.registered_providers:
raise ValueError('Entry point \'%s\' already registered' % entry_point)
entry_point_name = EntryPoint.parse(entry_point).name
if entry_point_name in self.available_providers:
raise ValueError('An entry point with name \'%s\' already registered' % entry_point_name)
self.registered_providers.insert(0, entry_point)
def unregister(self, entry_point):
"""Unregister a provider
:param string entry_point: provider to unregister (entry point syntax)
"""
self.registered_providers.remove(entry_point)
def __contains__(self, name):
return name in self.providers
provider_manager = LegacyProviderManager()
class PatchedProviderPool(ProviderPool):
"""
this is the subliminal ProviderPool but slightly patched to use our LegacyProviderManager,
because the new ProviderManager in the current subliminal package relies on stevedore, which has
problems detecting subliminal's provider extensions when running in the Plex sandbox
"""
def __init__(self, providers=None, provider_configs=None):
#: Name of providers to use
self.providers = providers or provider_manager.available_providers
#: Provider configuration
self.provider_configs = provider_configs or {}
#: Initialized providers
self.initialized_providers = {}
#: Discarded providers
self.discarded_providers = set()
#: Dedicated :data:`provider_manager` as :class:`~stevedore.enabled.EnabledExtensionManager`
# self.manager = EnabledExtensionManager(provider_manager.namespace, lambda e: e.name in self.providers)
self.manager = provider_manager if not providers else LegacyProviderManager(self.providers)
def list_subtitles(self, video, languages):
"""List subtitles.
:param video: video to list subtitles for.
:type video: :class:`~subliminal.video.Video`
:param languages: languages to search for.
:type languages: set of :class:`~babelfish.language.Language`
:return: found subtitles.
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
"""
subtitles = []
for name in self.providers:
# check discarded providers
if name in self.discarded_providers:
logger.debug('Skipping discarded provider %r', name)
continue
# check video validity
if not self.manager[name].plugin.check(video):
logger.info('Skipping provider %r: not a valid video', name)
continue
# check supported languages
provider_languages = self.manager[name].plugin.languages & languages
if not provider_languages:
logger.info('Skipping provider %r: no language to search for', name)
continue
# list subtitles
logger.info('Listing subtitles with provider %r and languages %r', name, provider_languages)
try:
provider_subtitles = self[name].list_subtitles(video, provider_languages)
except (requests.Timeout, socket.timeout):
logger.error('Provider %r timed out, discarding it', name)
self.discarded_providers.add(name)
continue
except LanguageReverseError, e:
logger.exception("Unexpected language reverse error in %s, skipping. Error: %s", name, traceback.format_exc())
continue
except Exception, e:
logger.exception('Unexpected error in provider %r, discarding it, because of: %s', name, traceback.format_exc())
self.discarded_providers.add(name)
continue
subtitles.extend(provider_subtitles)
return subtitles
def download_subtitle(self, subtitle):
"""Download `subtitle`'s :attr:`~subliminal.subtitle.Subtitle.content`.
:param subtitle: subtitle to download.
:type subtitle: :class:`~subliminal.subtitle.Subtitle`
:return: `True` if the subtitle has been successfully downloaded, `False` otherwise.
:rtype: bool
"""
# check discarded providers
if subtitle.provider_name in self.discarded_providers:
logger.warning('Provider %r is discarded', subtitle.provider_name)
return False
logger.info('Downloading subtitle %r', subtitle)
tries = 0
# retry downloading on failure until settings' download retry limit hit
while True:
tries += 1
try:
self[subtitle.provider_name].download_subtitle(subtitle)
except (requests.Timeout, socket.timeout):
logger.error('Provider %r timed out', subtitle.provider_name)
except ProviderError:
logger.error('Unexpected error in provider %r, Traceback: %s', subtitle.provider_name, traceback.format_exc())
break
except:
logger.exception('Unexpected error in provider %r, Traceback: %s', subtitle.provider_name, traceback.format_exc())
else:
break
if tries == DOWNLOAD_TRIES:
self.discarded_providers.add(subtitle.provider_name)
logger.error('Maximum retries reached for provider %r, discarding it', subtitle.provider_name)
return False
# don't hammer the provider
logger.debug('Errors while downloading subtitle, retrying provider %r in %s seconds', subtitle.provider_name, DOWNLOAD_RETRY_SLEEP)
time.sleep(DOWNLOAD_RETRY_SLEEP)
# check subtitle validity
if not subtitle.is_valid():
logger.error('Invalid subtitle')
return False
return True
def download_best_subtitles(self, subtitles, video, languages, min_score=0, hearing_impaired=False, only_one=False,
scores=None):
"""Download the best matching subtitles.
:param subtitles: the subtitles to use.
:type subtitles: list of :class:`~subliminal.subtitle.Subtitle`
:param video: video to download subtitles for.
:type video: :class:`~subliminal.video.Video`
:param languages: languages to download.
:type languages: set of :class:`~babelfish.language.Language`
:param int min_score: minimum score for a subtitle to be downloaded.
:param bool hearing_impaired: hearing impaired preference.
:param bool only_one: download only one subtitle, not one per language.
:param dict scores: scores to use, if `None`, the :attr:`~subliminal.video.Video.scores` from the video are
used.
:return: downloaded subtitles.
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
"""
use_hearing_impaired = hearing_impaired in ("prefer", "force HI")
# sort subtitles by score
unsorted_subtitles = []
for s in subtitles:
logger.debug("Starting score computation for %s", s)
try:
matches = s.get_matches(video, hearing_impaired=use_hearing_impaired)
except AttributeError:
logger.error("Match computation failed for %s: %s", s, traceback.format_exc())
continue
unsorted_subtitles.append((s, compute_score(matches, video, scores=scores), matches))
scored_subtitles = sorted(unsorted_subtitles, key=operator.itemgetter(1), reverse=True)
# download best subtitles, falling back on the next on error
downloaded_subtitles = []
for subtitle, score, matches in scored_subtitles:
# check score
if score < min_score:
logger.info('Score %d is below min_score (%d)', score, min_score)
break
# stop when all languages are downloaded
if set(s.language for s in downloaded_subtitles) == languages:
logger.debug('All languages downloaded')
break
# check downloaded languages
if subtitle.language in set(s.language for s in downloaded_subtitles):
logger.debug('Skipping subtitle: %r already downloaded', subtitle.language)
continue
# bail out if hearing_impaired was wrong
if "hearing_impaired" not in matches and hearing_impaired in ("force HI", "force non-HI"):
logger.debug('Skipping subtitle: %r with score %d because hearing-impaired set to %s', subtitle, score, hearing_impaired)
continue
# download
logger.info('Downloading subtitle %r with score %d', subtitle, score)
if self.download_subtitle(subtitle):
subtitle.score = score
downloaded_subtitles.append(subtitle)
# stop if only one subtitle is requested
if only_one:
logger.debug('Only one subtitle downloaded')
break
return downloaded_subtitles