Compare commits

..

4 Commits

Author SHA1 Message Date
Antoine Bertin 60610e2032 Merge branch 'develop'
Conflicts:
	requirements.txt
	setup.py
	subliminal/infos.py
2013-10-29 12:43:20 +01:00
Antoine Bertin 277b046b41 Fix requirements for enzyme 0.3 2013-05-19 15:44:49 +02:00
Antoine Bertin c823eda245 Update NEWS 2013-01-17 21:09:28 +01:00
Antoine Bertin 6340de0ddb Fix requirements due to requests 1.0 2013-01-17 20:49:41 +01:00
27 changed files with 304 additions and 923 deletions
+12 -33
View File
@@ -1,39 +1,6 @@
Changelog
=========
0.7.3
-----
**release date:** 2013-11-22
* Fix windows compatibility
* Improve subtitle validation
* Improve embedded subtitle languages detection
* Improve unittests
0.7.2
-----
**release date:** 2013-11-10
* Fix TVSubtitles for ambiguous series
* Add a CACHE_VERSION to force cache reloading on version change
* Set CLI default cache expiration time to 30 days
* Add podnapisi provider
* Support script for languages e.g. Latn, Cyrl
* Improve logging levels
* Fix subtitle validation in some rare cases
0.7.1
-----
**release date:** 2013-11-06
* Improve CLI
* Add login support for Addic7ed
* Remove lxml dependency
* Many fixes
0.7.0
-----
**release date:** 2013-10-29
@@ -52,6 +19,18 @@ Changelog
* Drop a few providers
* And much more...
0.6.4
-----
**release date:** 2013-05-19
* Fix requirements due to enzyme 0.3
0.6.3
-----
**release date:** 2013-01-17
* Fix requirements due to requests 1.0
0.6.2
-----
**release date:** 2012-09-15
+1 -2
View File
@@ -19,7 +19,6 @@ best matching subtitles. Providers are extensible through a dedicated entry poin
* Addic7ed
* BierDopje
* OpenSubtitles
* Podnapisi
* TheSubDB
* TvSubtitles
@@ -46,7 +45,7 @@ skipping videos that already have subtitles whether they are embedded or not::
subliminal.cache_region.configure('dogpile.cache.dbm', arguments={'filename': '/path/to/cachefile.dbm'})
# scan for videos in the folder and their subtitles
videos = subliminal.scan_videos(['/path/to/video/folder'], subtitles=True, embedded_subtitles=True, age=timedelta(weeks=1))
videos = scan_videos(['/path/to/video/folder'], subtitles=True, embedded_subtitles=True)
# download
subliminal.download_best_subtitles(videos, {Language('eng'), Language('fra')}, age=timedelta(week=1))
-2
View File
@@ -1,5 +1,3 @@
sympy>=0.7.3
sphinx>=1.1.3
sphinxcontrib-programoutput>=0.8
Sphinx-PyPI-upload>=0.2.1
setuptools>=1.4
+2 -6
View File
@@ -2,10 +2,6 @@ Cache
=====
.. module:: subliminal.cache
.. autodata:: CACHE_VERSION
.. autofunction:: subliminal_key_generator
.. data:: region
.. autodata:: region
The dogpile.cache region
Refer to `dogpile.cache's documentation <http://dogpilecache.readthedocs.org>`_ to see how to configure the region
Refer to `dogpile.cache's documentation <http://dogpilecache.readthedocs.org>`_ to see how to configure a region
+1
View File
@@ -4,6 +4,7 @@ Video
.. autodata:: VIDEO_EXTENSIONS
.. autodata:: SUBTITLE_EXTENSIONS
.. autodata:: LANGUAGE_EXTENSIONS
.. autoclass:: Video
:members:
.. autoclass:: Episode
+1 -2
View File
@@ -19,7 +19,6 @@ best matching subtitles. Providers are extensible through a dedicated entry poin
* Addic7ed
* BierDopje
* OpenSubtitles
* Podnapisi
* TheSubDB
* TvSubtitles
@@ -48,7 +47,7 @@ skipping videos that already have subtitles whether they are embedded or not::
subliminal.cache_region.configure('dogpile.cache.dbm', arguments={'filename': '/path/to/cachefile.dbm'})
# scan for videos in the folder and their subtitles
videos = subliminal.scan_videos(['/path/to/video/folder'], subtitles=True, embedded_subtitles=True, age=timedelta(weeks=1))
videos = scan_videos(['/path/to/video/folder'], subtitles=True, embedded_subtitles=True)
# download
subliminal.download_best_subtitles(videos, {Language('eng'), Language('fra')}, age=timedelta(week=1))
+6 -5
View File
@@ -1,9 +1,10 @@
beautifulsoup4>=4.3.2
guessit>=0.6.2
requests>=2.0.1
enzyme>=0.4.0
guessit>=0.6.1
requests>=2.0.0
enzyme>=0.3.1
html5lib>=0.99
dogpile.cache>=0.5.2
babelfish>=0.4.0
dogpile.cache>=0.5.1
babelfish>=0.1.5
lxml>=3.2.3
charade>=1.0.3
pysrt>=0.5.0
+3 -5
View File
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
setup(name='subliminal',
version='0.7.3',
version='0.7.0',
license='MIT',
description='Subtitles, faster than your thoughts',
long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(),
@@ -29,12 +29,10 @@ setup(name='subliminal',
'subliminal.providers': ['addic7ed = subliminal.providers.addic7ed:Addic7edProvider',
'bierdopje = subliminal.providers.bierdopje:BierDopjeProvider',
'opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider',
'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider',
'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider',
'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider'],
'babelfish.language_converters': ['addic7ed = subliminal.converters.addic7ed:Addic7edConverter',
'podnapisi = subliminal.converters.podnapisi:PodnapisiConverter',
'tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter']
'babelfish.converters': ['addic7ed = subliminal.converters.addic7ed:Addic7edConverter',
'tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter']
},
install_requires=open('requirements.txt').readlines(),
test_suite='subliminal.tests.suite')
+2 -2
View File
@@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
__title__ = 'subliminal'
__version__ = '0.7.3'
__version__ = '0.7.0'
__author__ = 'Antoine Bertin'
__license__ = 'MIT'
__copyright__ = 'Copyright 2013 Antoine Bertin'
import logging
from .api import PROVIDERS_ENTRY_POINT, list_subtitles, download_subtitles, download_best_subtitles
from .cache import MutexLock, region as cache_region
from .cache import region as cache_region
from .exceptions import Error, ProviderError, ProviderConfigurationError, ProviderNotAvailable, InvalidSubtitle
from .subtitle import Subtitle
from .video import VIDEO_EXTENSIONS, SUBTITLE_EXTENSIONS, Video, Episode, Movie, scan_videos, scan_video
+33 -59
View File
@@ -4,7 +4,6 @@ import collections
import io
import logging
import operator
import babelfish
import pkg_resources
from .exceptions import ProviderNotAvailable, InvalidSubtitle
from .subtitle import get_subtitle_path
@@ -34,49 +33,40 @@ def list_subtitles(videos, languages, providers=None, provider_configs=None):
provider_configs = provider_configs or {}
subtitles = collections.defaultdict(list)
# filter videos
videos = [v for v in videos if v.subtitle_languages & languages < languages]
videos = [v for v in videos if v.subtitle_languages != languages]
if not videos:
logger.info('No video to download subtitles for with languages %r', languages)
return subtitles
subtitle_languages = set.intersection(*[v.subtitle_languages for v in videos])
for provider_entry_point in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT):
# filter and initialize provider
if providers is not None and provider_entry_point.name not in providers:
logger.debug('Skipping provider %r: not in the list', provider_entry_point.name)
continue
Provider = provider_entry_point.load()
provider_languages = Provider.languages & languages - subtitle_languages
provider_languages = Provider.languages & languages
if not provider_languages:
logger.info('Skipping provider %r: no language to search for', provider_entry_point.name)
logger.debug('Skipping provider %r: no language to search for', provider_entry_point.name)
continue
provider_videos = [v for v in videos if Provider.check(v)]
if not provider_videos:
logger.info('Skipping provider %r: no video to search for', provider_entry_point.name)
logger.debug('Skipping provider %r: no video to search for', provider_entry_point.name)
continue
# list subtitles with the provider
try:
with Provider(**provider_configs.get(provider_entry_point.name, {})) as provider:
for provider_video in provider_videos:
provider_video_languages = provider_languages - provider_video.subtitle_languages
if not provider_video_languages:
logger.debug('Skipping provider %r: no language to search for for video %r',
provider_entry_point.name, provider_video)
continue
logger.info('Listing subtitles with provider %r for video %r with languages %r',
provider_entry_point.name, provider_video, provider_video_languages)
try:
provider_subtitles = provider.list_subtitles(provider_video, provider_video_languages)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
break
except:
logger.exception('Unexpected error in provider %r', provider_entry_point.name)
continue
logger.info('Found %d subtitles', len(provider_subtitles))
subtitles[provider_video].extend(provider_subtitles)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
with Provider(**provider_configs.get(provider_entry_point.name, {})) as provider:
for provider_video in provider_videos:
logger.info('Listing subtitles with provider %r for video %r with languages %r',
provider_entry_point.name, provider_video, provider_languages)
try:
provider_subtitles = provider.list_subtitles(provider_video, provider_languages)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
break
except:
logger.exception('Unexpected error in provider %r', provider_entry_point.name)
continue
logger.info('Found %d subtitles', len(provider_subtitles))
subtitles[provider_video].extend(provider_subtitles)
return subtitles
@@ -134,19 +124,14 @@ def download_subtitles(subtitles, provider_configs=None, single=False):
except:
logger.exception('Unexpected error in provider %r', subtitle.provider_name)
continue
with io.open(subtitle_path, 'w', encoding='utf-8') as f:
with io.open(subtitle_path, 'w') as f:
f.write(subtitle_text)
downloaded_languages.add(subtitle.language)
if single or downloaded_languages == languages:
break
finally: # terminate providers
for (provider_name, provider) in initialized_providers.items():
try:
provider.terminate()
except ProviderNotAvailable:
logger.warning('Provider %r is not available, unable to terminate', provider_name)
except:
logger.exception('Unexpected error in provider %r', provider_name)
for provider in initialized_providers.values():
provider.terminate()
def download_best_subtitles(videos, languages, providers=None, provider_configs=None, single=False, min_score=0,
@@ -170,24 +155,22 @@ def download_best_subtitles(videos, languages, providers=None, provider_configs=
discarded_providers = set()
downloaded_subtitles = collections.defaultdict(list)
# filter videos
videos = [v for v in videos if v.subtitle_languages & languages < languages
and (not single or babelfish.Language('und') not in v.subtitle_languages)]
videos = [v for v in videos if v.subtitle_languages != languages]
if not videos:
logger.info('No video to download subtitles for with languages %r', languages)
return downloaded_subtitles
# filter and initialize providers
subtitle_languages = set.intersection(*[v.subtitle_languages for v in videos])
initialized_providers = {}
for provider_entry_point in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT):
if providers is not None and provider_entry_point.name not in providers:
logger.debug('Skipping provider %r: not in the list', provider_entry_point.name)
continue
Provider = provider_entry_point.load()
if not Provider.languages & languages - subtitle_languages:
logger.info('Skipping provider %r: no language to search for', provider_entry_point.name)
if not Provider.languages & languages:
logger.debug('Skipping provider %r: no language to search for', provider_entry_point.name)
continue
if not [v for v in videos if Provider.check(v)]:
logger.info('Skipping provider %r: no video to search for', provider_entry_point.name)
logger.debug('Skipping provider %r: no video to search for', provider_entry_point.name)
continue
provider = Provider(**provider_configs.get(provider_entry_point.name, {}))
try:
@@ -205,15 +188,11 @@ def download_best_subtitles(videos, languages, providers=None, provider_configs=
if provider_name in discarded_providers:
logger.debug('Skipping discarded provider %r', provider_name)
continue
provider_video_languages = provider.languages & languages - video.subtitle_languages
if not provider_video_languages:
logger.debug('Skipping provider %r: no language to search for for video %r', provider_name,
video)
continue
provider_languages = provider.languages & languages
logger.info('Listing subtitles with provider %r for video %r with languages %r',
provider_name, video, provider_video_languages)
provider_name, video, provider_languages)
try:
provider_subtitles = provider.list_subtitles(video, provider_video_languages)
provider_subtitles = provider.list_subtitles(video, provider_languages)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_name)
discarded_providers.add(provider_name)
@@ -225,7 +204,7 @@ def download_best_subtitles(videos, languages, providers=None, provider_configs=
subtitles.extend(provider_subtitles)
# find the best subtitles and download them
downloaded_languages = video.subtitle_languages.copy()
downloaded_languages = set()
for subtitle, score in sorted([(s, s.compute_score(video)) for s in subtitles],
key=operator.itemgetter(1), reverse=True):
# filter
@@ -259,18 +238,13 @@ def download_best_subtitles(videos, languages, providers=None, provider_configs=
except:
logger.exception('Unexpected error in provider %r', subtitle.provider_name)
continue
with io.open(subtitle_path, 'w', encoding='utf-8') as f:
with io.open(subtitle_path, 'w') as f:
f.write(subtitle_text)
downloaded_languages.add(subtitle.language)
if single or downloaded_languages >= languages:
if single or downloaded_languages == languages:
logger.debug('All languages downloaded')
break
finally: # terminate providers
for (provider_name, provider) in initialized_providers.items():
try:
provider.terminate()
except ProviderNotAvailable:
logger.warning('Provider %r is not available, unable to terminate', provider_name)
except:
logger.exception('Unexpected error in provider %r', provider_name)
for provider in initialized_providers.values():
provider.terminate()
return downloaded_subtitles
+3 -53
View File
@@ -1,56 +1,6 @@
# -*- coding: utf-8 -*-
import inspect
from dogpile.cache import make_region # @UnresolvedImport
from dogpile.cache.backends.file import AbstractFileLock # @UnresolvedImport
from dogpile.cache.compat import string_type # @UnresolvedImport
from dogpile.core.readwrite_lock import ReadWriteMutex # @UnresolvedImport
import dogpile.cache
#: Subliminal's cache version
CACHE_VERSION = 1
def subliminal_key_generator(namespace, fn, to_str=string_type):
"""Add a :data:`CACHE_VERSION` to dogpile.cache's default function_key_generator"""
if namespace is None:
namespace = '%d:%s:%s' % (CACHE_VERSION, fn.__module__, fn.__name__)
else:
namespace = '%d:%s:%s|%s' % (CACHE_VERSION, fn.__module__, fn.__name__, namespace)
args = inspect.getargspec(fn)
has_self = args[0] and args[0][0] in ('self', 'cls')
def generate_key(*args, **kw):
if kw:
raise ValueError('Keyword arguments not supported')
if has_self:
args = args[1:]
return namespace + '|' + ' '.join(map(to_str, args))
return generate_key
class MutexLock(AbstractFileLock):
""":class:`MutexLock` is a thread-based rw lock based on :class:`dogpile.core.ReadWriteMutex`"""
def __init__(self, filename):
self.mutex = ReadWriteMutex()
def acquire_read_lock(self, wait):
ret = self.mutex.acquire_read_lock(wait)
return wait or ret
def acquire_write_lock(self, wait):
ret = self.mutex.acquire_write_lock(wait)
return wait or ret
def release_read_lock(self):
return self.mutex.release_read_lock()
def release_write_lock(self):
return self.mutex.release_write_lock()
#: The dogpile.cache region (long-lived)
region = make_region(function_key_generator=subliminal_key_generator)
#: The dogpile.cache region for :meth:`~subliminal.providers.Provider.query` (short-lived)
query_region = make_region(function_key_generator=subliminal_key_generator)
#: The subliminal's dogpile.cache region
region = dogpile.cache.make_region()
+34 -110
View File
@@ -9,152 +9,76 @@ import sys
import babelfish
import guessit
import pkg_resources
from subliminal import (__version__, PROVIDERS_ENTRY_POINT, cache_region, MutexLock, Video, Episode, Movie, scan_videos,
from subliminal import (__version__, PROVIDERS_ENTRY_POINT, cache_region, Video, Episode, Movie, scan_videos,
download_best_subtitles)
try:
import colorlog
except ImportError:
colorlog = None
DEFAULT_CACHE_FILE = os.path.join('~', '.config', 'subliminal.cache.dbm')
def subliminal_parser():
parser = argparse.ArgumentParser(description='Subtitles, faster than your thoughts')
parser.add_argument('-l', '--languages', nargs='+', metavar='LANGUAGE', help='wanted languages as alpha2 code (ISO-639-1)')
parser.add_argument('-p', '--providers', nargs='+', metavar='PROVIDER', help='providers to use from %s (default: all)' % ', '.join(ep.name for ep in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT)))
parser.add_argument('-m', '--min-score', type=int, help='minimum score for subtitles. 0-%d for episodes, 0-%d for movies' % (Episode.scores['hash'], Movie.scores['hash']))
parser.add_argument('-s', '--single', action='store_true', help='download without language code in subtitle\'s filename i.e. .srt only')
parser.add_argument('-f', '--force', action='store_true', help='overwrite existing subtitles')
parser.add_argument('-c', '--cache-file', default=DEFAULT_CACHE_FILE, help='cache file (default: %(default)s)')
parser.add_argument('-a', '--age', help='download subtitles for videos newer than AGE e.g. 12h, 1w2d')
parser.add_argument('--hearing-impaired', action='store_true', help='download hearing impaired subtitles')
group_verbosity = parser.add_mutually_exclusive_group()
group_verbosity.add_argument('-q', '--quiet', action='store_true', help='disable output')
group_verbosity.add_argument('-v', '--verbose', action='store_true', help='verbose output')
parser.add_argument('--version', action='version', version=__version__)
parser.add_argument('paths', nargs='+', metavar='PATH', help='path to video file or folder')
return parser
def subliminal():
parser = argparse.ArgumentParser(prog='subliminal', description='Subtitles, faster than your thoughts',
epilog='Suggestions and bug reports are greatly appreciated: '
'https://github.com/Diaoul/subliminal/issues', add_help=False)
# required arguments
required_arguments_group = parser.add_argument_group('required arguments')
required_arguments_group.add_argument('paths', nargs='+', metavar='PATH', help='path to video file or folder')
required_arguments_group.add_argument('-l', '--languages', nargs='+', required=True, metavar='LANGUAGE',
help='wanted languages as IETF codes e.g. fr, pt-BR, sr-Cyrl ')
# configuration
configuration_group = parser.add_argument_group('configuration')
configuration_group.add_argument('-s', '--single', action='store_true',
help='download without language code in subtitle\'s filename i.e. .srt only')
configuration_group.add_argument('-c', '--cache-file', default=DEFAULT_CACHE_FILE,
help='cache file (default: %(default)s)')
# filtering
filtering_group = parser.add_argument_group('filtering')
providers = [ep.name for ep in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT)]
filtering_group.add_argument('-p', '--providers', nargs='+', metavar='PROVIDER',
help='providers to use (%s)' % ', '.join(providers))
filtering_group.add_argument('-m', '--min-score', type=int,
help='minimum score for subtitles (0-%d for episodes, 0-%d for movies)'
% (Episode.scores['hash'], Movie.scores['hash']))
filtering_group.add_argument('-a', '--age', help='download subtitles for videos newer than AGE e.g. 12h, 1w2d')
filtering_group.add_argument('-h', '--hearing-impaired', action='store_true',
help='download hearing impaired subtitles')
filtering_group.add_argument('-f', '--force', action='store_true',
help='force subtitle download for videos with existing subtitles')
# addic7ed
addic7ed_group = parser.add_argument_group('addic7ed')
addic7ed_group.add_argument('--addic7ed-username', metavar='USERNAME', help='username for addic7ed provider')
addic7ed_group.add_argument('--addic7ed-password', metavar='PASSWORD', help='password for addic7ed provider')
# output
output_group = parser.add_argument_group('output')
output_exclusive_group = output_group.add_mutually_exclusive_group()
output_exclusive_group.add_argument('-q', '--quiet', action='store_true', help='disable output')
output_exclusive_group.add_argument('-v', '--verbose', action='store_true', help='verbose output')
output_group.add_argument('--color', action='store_true', help='add color to console output (requires colorlog)')
# troubleshooting
troubleshooting_group = parser.add_argument_group('troubleshooting')
troubleshooting_group.add_argument('--debug', action='store_true', help='debug output')
troubleshooting_group.add_argument('--version', action='version', version=__version__)
troubleshooting_group.add_argument('--help', action='help', help='show this help message and exit')
# parse args
parser = subliminal_parser()
args = parser.parse_args()
# parse paths
try:
args.paths = [os.path.abspath(os.path.expanduser(p.decode('utf-8'))) for p in args.paths]
args.paths = [p.decode('utf-8') for p in args.paths]
except UnicodeDecodeError:
parser.error('argument paths: encodings is not utf-8: %r' % args.paths)
# parse languages
try:
args.languages = {babelfish.Language.fromietf(l) for l in args.languages}
args.languages = {babelfish.Language.fromalpha2(l) for l in args.languages}
except babelfish.Error:
parser.error('argument -l/--languages: codes are not IETF: %r' % args.languages)
parser.error('argument -l/--languages: codes are not ISO-639-1: %r' % args.languages)
# parse age
if args.age is not None:
match = re.match(r'^(?:(?P<weeks>\d+?)w)?(?:(?P<days>\d+?)d)?(?:(?P<hours>\d+?)h)?$', args.age)
if not match:
parser.error('argument -a/--age: invalid age: %r' % args.age)
args.age = datetime.timedelta(**{k: int(v) for k, v in match.groupdict(0).items()})
args.age = datetime.timedelta(**match.groupdict())
# parse cache-file
args.cache_file = os.path.abspath(os.path.expanduser(args.cache_file))
if not os.path.exists(os.path.split(args.cache_file)[0]):
parser.error('argument -c/--cache-file: directory %r for cache file does not exist'
% os.path.split(args.cache_file)[0])
# parse provider configs
provider_configs = {}
if (args.addic7ed_username is not None and args.addic7ed_password is None
or args.addic7ed_username is None and args.addic7ed_password is not None):
parser.error('argument --addic7ed-username/--addic7ed-password: both arguments are required or none')
if args.addic7ed_username is not None and args.addic7ed_password is not None:
provider_configs['addic7ed'] = {'username': args.addic7ed_username, 'password': args.addic7ed_password}
# parse color
if args.color and colorlog is None:
parser.error('argument --color: colorlog required')
# setup output
if args.debug:
handler = logging.StreamHandler()
if args.color:
handler.setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)-8s%(reset)s [%(blue)s%(name)s-%(funcName)s:%(lineno)d%(reset)s] %(message)s',
log_colors=dict(colorlog.default_log_colors.items() + [('DEBUG', 'cyan')])))
else:
handler.setFormatter(logging.Formatter('%(levelname)-8s [%(name)s-%(funcName)s:%(lineno)d] %(message)s'))
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.DEBUG)
elif args.verbose:
handler = logging.StreamHandler()
if args.color:
handler.setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)-8s%(reset)s [%(blue)s%(name)s%(reset)s] %(message)s'))
else:
handler.setFormatter(logging.Formatter('%(levelname)-8s [%(name)s] %(message)s'))
logging.getLogger('subliminal').addHandler(handler)
logging.getLogger('subliminal').setLevel(logging.INFO)
# setup verbosity
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
elif not args.quiet:
handler = logging.StreamHandler()
if args.color:
handler.setFormatter(colorlog.ColoredFormatter('[%(log_color)s%(levelname)s%(reset)s] %(message)s'))
else:
handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
logging.getLogger('subliminal.api').addHandler(handler)
logging.getLogger('subliminal.api').setLevel(logging.INFO)
logging.basicConfig(level=logging.WARN)
# configure cache
cache_region.configure('dogpile.cache.dbm', expiration_time=datetime.timedelta(days=30), # @UndefinedVariable
arguments={'filename': args.cache_file, 'lock_factory': MutexLock})
cache_region.configure('dogpile.cache.dbm', arguments={'filename': os.path.expanduser(args.cache_file)})
# scan videos
videos = scan_videos([p for p in args.paths if os.path.exists(p)], subtitles=not args.force,
embedded_subtitles=not args.force, age=args.age)
videos = scan_videos([p for p in args.paths if os.path.exists(p)], subtitles=not args.force, age=args.age)
# guess videos
videos.extend([Video.fromguess(os.path.split(p)[1], guessit.guess_file_info(p, 'autodetect')) for p in args.paths
if not os.path.exists(p)])
# download best subtitles
subtitles = download_best_subtitles(videos, args.languages, providers=args.providers,
provider_configs=provider_configs, single=args.single,
min_score=args.min_score, hearing_impaired=args.hearing_impaired)
subtitles = download_best_subtitles(videos, args.languages, providers=args.providers, provider_configs=None,
single=args.single, min_score=args.min_score,
hearing_impaired=args.hearing_impaired)
# result output
# output result
if not subtitles:
if not args.quiet:
sys.stderr.write('No subtitles downloaded\n')
+17 -19
View File
@@ -1,31 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from babelfish import LanguageReverseConverter, get_language_converter
from babelfish.converters.name import NameConverter
class Addic7edConverter(LanguageReverseConverter):
class Addic7edConverter(NameConverter):
def __init__(self):
self.name_converter = get_language_converter('name')
self.from_addic7ed = {'Català': ('cat',), 'Chinese (Simplified)': ('zho',), 'Chinese (Traditional)': ('zho',),
'Euskera': ('eus',), 'Galego': ('glg',), 'Greek': ('ell',), 'Malay': ('msa',),
'Portuguese (Brazilian)': ('por', 'BR'), 'Serbian (Cyrillic)': ('srp', None, 'Cyrl'),
'Serbian (Latin)': ('srp',), 'Spanish (Latin America)': ('spa',),
'Spanish (Spain)': ('spa',)}
self.to_addic7ed = {('cat',): 'Català', ('zho',): 'Chinese (Simplified)', ('eus',): 'Euskera',
('glg',): 'Galego', ('ell',): 'Greek', ('msa',): 'Malay',
('por', 'BR'): 'Portuguese (Brazilian)', ('srp', None, 'Cyrl'): 'Serbian (Cyrillic)'}
self.codes = self.name_converter.codes | set(self.from_addic7ed.keys())
super(Addic7edConverter, self).__init__()
self.from_addic7ed = {'Català': ('cat', None), 'Chinese (Simplified)': ('zho', None),
'Chinese (Traditional)': ('zho', None), 'Euskera': ('eus', None),
'Galego': ('glg', None), 'Greek': ('ell', None),
'Malay': ('msa', None), 'Portuguese (Brazilian)': ('por', 'BR'),
'Serbian (Cyrillic)': ('srp', None), 'Serbian (Latin)': ('srp', None),
'Spanish (Latin America)': ('spa', None), 'Spanish (Spain)': ('spa', None)}
self.to_addic7ed = {('cat', None): 'Català', ('zho', None): 'Chinese (Simplified)',
('eus', None): 'Euskera', ('glg', None): 'Galego',
('ell', None): 'Greek', ('msa', None): 'Malay',
('por', 'BR'): 'Portuguese (Brazilian)', ('srp', None): 'Serbian (Cyrillic)'}
self.codes |= set(self.from_addic7ed.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3, country, script) in self.to_addic7ed:
return self.to_addic7ed[(alpha3, country, script)]
def convert(self, alpha3, country=None):
if (alpha3, country) in self.to_addic7ed:
return self.to_addic7ed[(alpha3, country)]
if (alpha3,) in self.to_addic7ed:
return self.to_addic7ed[(alpha3,)]
return self.name_converter.convert(alpha3, country, script)
return super(Addic7edConverter, self).convert(alpha3, country)
def reverse(self, addic7ed):
if addic7ed in self.from_addic7ed:
return self.from_addic7ed[addic7ed]
return self.name_converter.reverse(addic7ed)
return super(Addic7edConverter, self).reverse(addic7ed)
-32
View File
@@ -1,32 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from babelfish import LanguageReverseConverter, LanguageConvertError, LanguageReverseError
class PodnapisiConverter(LanguageReverseConverter):
def __init__(self):
self.from_podnapisi = {2: ('eng',), 28: ('spa',), 26: ('pol',), 36: ('srp',), 1: ('slv',), 38: ('hrv',),
9: ('ita',), 8: ('fra',), 48: ('por', 'BR'), 23: ('nld',), 12: ('ara',), 13: ('ron',),
33: ('bul',), 32: ('por',), 16: ('ell',), 15: ('hun',), 31: ('fin',), 30: ('tur',),
7: ('ces',), 25: ('swe',), 27: ('rus',), 24: ('dan',), 22: ('heb',), 51: ('vie',),
52: ('fas',), 5: ('deu',), 14: ('spa', 'AR'), 54: ('ind',), 47: ('srp', None, 'Cyrl'),
3: ('nor',), 20: ('est',), 10: ('bos',), 17: ('zho',), 37: ('slk',), 35: ('mkd',),
11: ('jpn',), 4: ('kor',), 29: ('sqi',), 6: ('isl',), 19: ('lit',), 46: ('ukr',),
44: ('tha',), 53: ('cat',), 56: ('sin',), 21: ('lav',), 40: ('cmn',), 55: ('msa',),
42: ('hin',), 50: ('bel',)}
self.to_podnapisi = {v: k for k, v in self.from_podnapisi.items()}
self.codes = set(self.from_podnapisi.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3,) in self.to_podnapisi:
return self.to_podnapisi[(alpha3,)]
if (alpha3, country) in self.to_podnapisi:
return self.to_podnapisi[(alpha3, country)]
if (alpha3, country, script) in self.to_podnapisi:
return self.to_podnapisi[(alpha3, country, script)]
raise LanguageConvertError(alpha3, country, script)
def reverse(self, podnapisi):
if podnapisi not in self.from_podnapisi:
raise LanguageReverseError(podnapisi)
return self.from_podnapisi[podnapisi]
+9 -11
View File
@@ -1,24 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from babelfish import LanguageReverseConverter, get_language_converter
from babelfish.converters.alpha2 import Alpha2Converter
class TVsubtitlesConverter(LanguageReverseConverter):
class TVsubtitlesConverter(Alpha2Converter):
def __init__(self):
self.alpha2_converter = get_language_converter('alpha2')
self.from_tvsubtitles = {'br': ('por', 'BR'), 'ua': ('ukr',), 'gr': ('ell',), 'cn': ('zho',), 'jp': ('jpn',),
'cz': ('ces',)}
super(TVsubtitlesConverter, self).__init__()
self.from_tvsubtitles = {'br': ('por', 'BR'), 'ua': ('ukr', None), 'gr': ('ell', None), 'cn': ('zho', None),
'jp': ('jpn', None), 'cz': ('ces', None)}
self.to_tvsubtitles = {v: k for k, v in self.from_tvsubtitles}
self.codes = self.alpha2_converter.codes | set(self.from_tvsubtitles.keys())
self.codes |= set(self.from_tvsubtitles.keys())
def convert(self, alpha3, country=None, script=None):
def convert(self, alpha3, country=None):
if (alpha3, country) in self.to_tvsubtitles:
return self.to_tvsubtitles[(alpha3, country)]
if (alpha3,) in self.to_tvsubtitles:
return self.to_tvsubtitles[(alpha3,)]
return self.alpha2_converter.convert(alpha3, country, script)
return super(TVsubtitlesConverter, self).convert(alpha3, country)
def reverse(self, tvsubtitles):
if tvsubtitles in self.from_tvsubtitles:
return self.from_tvsubtitles[tvsubtitles]
return self.alpha2_converter.reverse(tvsubtitles)
return super(TVsubtitlesConverter, self).reverse(tvsubtitles)
+3 -37
View File
@@ -8,7 +8,7 @@ import requests
from . import Provider
from .. import __version__
from ..cache import region
from ..exceptions import ProviderConfigurationError, ProviderNotAvailable, InvalidSubtitle
from ..exceptions import InvalidSubtitle, ProviderNotAvailable
from ..subtitle import Subtitle, is_valid_subtitle
from ..video import Episode
@@ -58,43 +58,14 @@ class Addic7edProvider(Provider):
'fin', 'fra', 'glg', 'heb', 'hrv', 'hun', 'hye', 'ind', 'ita', 'jpn', 'kor', 'mkd', 'msa',
'nld', 'nor', 'pol', 'por', 'ron', 'rus', 'slk', 'slv', 'spa', 'sqi', 'srp', 'swe', 'tha',
'tur', 'ukr', 'vie', 'zho']}
video_types = (Episode,)
videos = (Episode,)
server = 'http://www.addic7ed.com'
def __init__(self, username=None, password=None):
if username is not None and password is None or username is None and password is not None:
raise ProviderConfigurationError('Username and password must be specified')
self.username = username
self.password = password
self.logged_in = False
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
# login
if self.username is not None and self.password is not None:
logger.debug('Logging in')
data = {'username': self.username, 'password': self.password, 'Submit': 'Log in'}
try:
r = self.session.post(self.server + '/dologin.php', data, timeout=10, allow_redirects=False)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code == 302:
logger.info('Logged in')
self.logged_in = True
else:
logger.error('Failed to login')
def terminate(self):
# logout
if self.logged_in:
try:
r = self.session.get(self.server + '/logout.php', timeout=10)
logger.info('Logged out')
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
self.session.close()
def get(self, url, params=None):
@@ -166,9 +137,6 @@ class Addic7edProvider(Provider):
if cells[5].string != 'Completed':
logger.debug('Skipping incomplete subtitle')
continue
if not cells[3].string:
logger.debug('Skipping empty language')
continue
subtitles.append(Addic7edSubtitle(babelfish.Language.fromaddic7ed(cells[3].string), series, season,
int(cells[1].string), cells[2].string, cells[4].string,
bool(cells[6].string), cells[9].a['href'], link))
@@ -186,9 +154,7 @@ class Addic7edProvider(Provider):
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
if r.headers['Content-Type'] == 'text/html':
raise ProviderNotAvailable('Download limit exceeded')
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'])
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+16 -13
View File
@@ -3,10 +3,10 @@ from __future__ import unicode_literals
import logging
import urllib
import babelfish
import bs4
import charade
import guessit
import requests
import xml.etree.ElementTree
from . import Provider
from .. import __version__
from ..cache import region
@@ -65,7 +65,7 @@ class BierDopjeProvider(Provider):
:param string url: API part of the URL to reach without the leading slash
:param \*\*params: format specs for the `url`
:return: the response
:rtype: :class:`xml.etree.ElementTree.Element`
:rtype: :class:`bs4.BeautifulSoup`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
@@ -77,7 +77,7 @@ class BierDopjeProvider(Provider):
raise ProviderNotAvailable('Too Many Requests')
elif r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
return xml.etree.ElementTree.fromstring(r.content)
return bs4.BeautifulSoup(r.content, ['xml'])
@region.cache_on_arguments()
def find_show_id(self, series):
@@ -89,11 +89,11 @@ class BierDopjeProvider(Provider):
"""
logger.debug('Searching for series %r', series)
root = self.get('FindShowByName/{series}', series=urllib.quote(series))
if root.find('response/status').text == 'false':
soup = self.get('FindShowByName/{series}', series=urllib.quote(series))
if soup.status.contents[0] == 'false':
logger.info('Series %r not found', series)
return None
return int(root.find('response/results/result[1]/showid').text)
return int(soup.showid.contents[0])
def query(self, language, season, episode, tvdb_id=None, series=None):
params = {'language': language.alpha2, 'season': season, 'episode': episode}
@@ -109,16 +109,19 @@ class BierDopjeProvider(Provider):
else:
raise ValueError('Missing parameter tvdb_id or series')
logger.debug('Searching subtitles %r', params)
root = self.get('GetAllSubsFor/{showid}/{season}/{episode}/{language}/{istvdbid}', **params)
if root.find('response/status').text == 'false':
soup = self.get('GetAllSubsFor/{showid}/{season}/{episode}/{language}/{istvdbid}', **params)
if soup.status.contents[0] == 'false':
logger.debug('No subtitle found')
return []
logger.debug('Found subtitles %r', root.find('response/results'))
return [BierDopjeSubtitle(language, season, episode, tvdb_id, series, result.find('filename').text,
result.find('downloadlink').text) for result in root.find('response/results')]
logger.debug('Found subtitles %r', soup.results('result'))
return [BierDopjeSubtitle(language, season, episode, tvdb_id, series, result.filename.contents[0],
result.downloadlink.contents[0]) for result in soup.results('result')]
def list_subtitles(self, video, languages):
return [s for l in languages for s in self.query(l, video.season, video.episode, video.tvdb_id, video.series)]
subtitles = []
for language in languages:
subtitles.extend(self.query(language, video.season, video.episode, video.tvdb_id, video.series))
return subtitles
def download_subtitle(self, subtitle):
try:
@@ -129,7 +132,7 @@ class BierDopjeProvider(Provider):
raise ProviderNotAvailable('Too Many Requests')
elif r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'])
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+7 -7
View File
@@ -23,8 +23,8 @@ class OpenSubtitlesSubtitle(Subtitle):
provider_name = 'opensubtitles'
series_re = re.compile('^"(?P<series_name>.*)" (?P<series_title>.*)$')
def __init__(self, language, hearing_impaired, id, matched_by, movie_kind, hash, movie_name, movie_release_name, # @ReservedAssignment
movie_year, movie_imdb_id, series_season, series_episode):
def __init__(self, language, hearing_impaired, id, matched_by, movie_kind, hash, movie_name, movie_release_name, movie_year,
movie_imdb_id, series_season, series_episode):
super(OpenSubtitlesSubtitle, self).__init__(language, hearing_impaired)
self.id = id
self.matched_by = matched_by
@@ -83,7 +83,7 @@ class OpenSubtitlesSubtitle(Subtitle):
class OpenSubtitlesProvider(Provider):
languages = {babelfish.Language.fromopensubtitles(l) for l in babelfish.get_language_converter('opensubtitles').codes}
languages = {babelfish.Language.fromopensubtitles(l) for l in babelfish.CONVERTERS['opensubtitles'].codes}
def __init__(self):
self.server = xmlrpclib.ServerProxy('http://api.opensubtitles.org/xml-rpc')
@@ -106,7 +106,7 @@ class OpenSubtitlesProvider(Provider):
if response['status'] != '200 OK':
raise ProviderError('Logout failed with status %r' % response['status'])
def query(self, languages, hash=None, size=None, imdb_id=None, query=None): # @ReservedAssignment
def query(self, languages, hash=None, size=None, imdb_id=None, query=None):
searches = []
if hash and size:
searches.append({'moviehash': hash, 'moviebytesize': str(size)})
@@ -128,6 +128,7 @@ class OpenSubtitlesProvider(Provider):
if not response['data']:
logger.debug('No subtitle found')
return []
logger.debug('Found subtitles %r', response['data'])
return [OpenSubtitlesSubtitle(babelfish.Language.fromopensubtitles(r['SubLanguageID']),
bool(int(r['SubHearingImpaired'])), r['IDSubtitleFile'], r['MatchedBy'],
r['MovieKind'], r['MovieHash'], r['MovieName'], r['MovieReleaseName'],
@@ -140,8 +141,7 @@ class OpenSubtitlesProvider(Provider):
query = None
if ('opensubtitles' not in video.hashes or not video.size) and not video.imdb_id:
query = video.name.split(os.sep)[-1]
return self.query(languages, hash=video.hashes.get('opensubtitles'), size=video.size, imdb_id=video.imdb_id,
query=query)
return self.query(languages, hash=video.hashes.get('opensubtitles'), size=video.size, imdb_id=video.imdb_id, query=query)
def download_subtitle(self, subtitle):
try:
@@ -153,7 +153,7 @@ class OpenSubtitlesProvider(Provider):
if not response['data']:
raise ProviderError('Nothing to download')
subtitle_bytes = zlib.decompress(base64.b64decode(response['data'][0]['data']), 47)
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'], 'replace')
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'])
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
-163
View File
@@ -1,163 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import io
import logging
import re
import xml.etree.ElementTree
import zipfile
import babelfish
import bs4
import charade
import guessit
import requests
from . import Provider
from .. import __version__
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
from ..subtitle import Subtitle, is_valid_subtitle, compute_guess_matches
from ..video import Episode, Movie
logger = logging.getLogger(__name__)
class PodnapisiSubtitle(Subtitle):
provider_name = 'podnapisi'
def __init__(self, language, id, releases, hearing_impaired, link, series=None, season=None, episode=None, # @ReservedAssignment
title=None, year=None):
super(PodnapisiSubtitle, self).__init__(language, hearing_impaired)
self.id = id
self.releases = releases
self.hearing_impaired = hearing_impaired
self.link = link
self.series = series
self.season = season
self.episode = episode
self.title = title
self.year = year
def compute_matches(self, video):
matches = set()
# episode
if isinstance(video, Episode):
# series
if video.series and self.series.lower() == video.series.lower():
matches.add('series')
# season
if video.season and self.season == video.season:
matches.add('season')
# episode
if video.episode and self.episode == video.episode:
matches.add('episode')
# guess
for release in self.releases:
matches |= compute_guess_matches(video, guessit.guess_episode_info(release + '.mkv'))
# movie
elif isinstance(video, Movie):
# title
if video.title and self.title.lower() == video.title.lower():
matches.add('title')
# year
if video.year and self.year == video.year:
matches.add('year')
# guess
for release in self.releases:
matches |= compute_guess_matches(video, guessit.guess_movie_info(release + '.mkv'))
return matches
class PodnapisiProvider(Provider):
languages = {babelfish.Language.frompodnapisi(l) for l in babelfish.get_language_converter('podnapisi').codes}
video_types = (Episode, Movie)
server = 'http://simple.podnapisi.net'
link_re = re.compile('^.*(?P<link>/ppodnapisi/download/i/\d+/k/.*$)')
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
def terminate(self):
self.session.close()
def get(self, url, params=None, is_xml=True):
"""Make a GET request on `url` with the given parameters
:param string url: part of the URL to reach with the leading slash
:param dict params: params of the request
:param bool xml: whether the response content is XML or not
:return: the response
:rtype: :class:`xml.etree.ElementTree.Element` or :class:`bs4.BeautifulSoup`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
try:
r = self.session.get(self.server + '/ppodnapisi' + url, params=params, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
if is_xml:
return xml.etree.ElementTree.fromstring(r.content)
else:
return bs4.BeautifulSoup(r.content, ['permissive'])
def query(self, language, series=None, season=None, episode=None, title=None, year=None):
params = {'sXML': 1, 'sJ': language.podnapisi}
if series and season and episode:
params['sK'] = series
params['sTS'] = season
params['sTE'] = episode
elif title:
params['sK'] = title
if year:
params['sY'] = year
else:
raise ValueError('Missing parameters series and season and episode or title')
logger.debug('Searching episode %r', params)
subtitles = []
while True:
root = self.get('/search', params)
if not int(root.find('pagination/results').text):
logger.debug('No subtitle found')
break
if series and season and episode:
subtitles.extend([PodnapisiSubtitle(language, int(s.find('id').text), s.find('release').text.split(),
'h' in (s.find('flags').text or ''), s.find('url').text[38:],
series=series, season=season, episode=episode)
for s in root.findall('subtitle')])
elif title:
subtitles.extend([PodnapisiSubtitle(language, int(s.find('id').text), s.find('release').text.split(),
'h' in (s.find('flags').text or ''), s.find('url').text[38:],
title=title, year=year)
for s in root.findall('subtitle')])
if int(root.find('pagination/current').text) >= int(root.find('pagination/count').text):
break
params['page'] = int(root.find('pagination/current').text) + 1
return subtitles
def list_subtitles(self, video, languages):
if isinstance(video, Episode):
return [s for l in languages for s in self.query(l, series=video.series, season=video.season,
episode=video.episode)]
elif isinstance(video, Movie):
return [s for l in languages for s in self.query(l, title=video.title, year=video.year)]
def download_subtitle(self, subtitle):
soup = self.get(subtitle.link, is_xml=False)
link = soup.find('a', href=self.link_re)
if not link:
raise ProviderError('Cannot find the download link')
try:
r = self.session.get(self.server + self.link_re.match(link['href']).group('link'), timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
with zipfile.ZipFile(io.BytesIO(r.content)) as zf:
if len(zf.namelist()) > 1:
raise ProviderError('More than one file to unzip')
subtitle_bytes = zf.read(zf.namelist()[0])
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+5 -3
View File
@@ -8,6 +8,7 @@ from . import Provider
from .. import __version__
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
from ..subtitle import Subtitle, is_valid_subtitle
from ..video import Episode, Movie
logger = logging.getLogger(__name__)
@@ -16,7 +17,7 @@ logger = logging.getLogger(__name__)
class TheSubDBSubtitle(Subtitle):
provider_name = 'thesubdb'
def __init__(self, language, hash): # @ReservedAssignment
def __init__(self, language, hash):
super(TheSubDBSubtitle, self).__init__(language)
self.hash = hash
@@ -30,6 +31,7 @@ class TheSubDBSubtitle(Subtitle):
class TheSubDBProvider(Provider):
languages = {babelfish.Language.fromalpha2(l) for l in ['en', 'es', 'fr', 'it', 'nl', 'pl', 'pt', 'ro', 'sv', 'tr']}
video_types = (Episode, Movie)
required_hash = 'thesubdb'
def initialize(self):
@@ -55,7 +57,7 @@ class TheSubDBProvider(Provider):
raise ProviderNotAvailable('Timeout after 10 seconds')
return r
def query(self, hash): # @ReservedAssignment
def query(self, hash):
params = {'action': 'search', 'hash': hash}
logger.debug('Searching subtitles %r', params)
r = self.get(params)
@@ -75,7 +77,7 @@ class TheSubDBProvider(Provider):
r = self.get(params)
if r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'])
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+5 -14
View File
@@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
class TVsubtitlesSubtitle(Subtitle):
provider_name = 'tvsubtitles'
def __init__(self, language, series, season, episode, id, rip, release): # @ReservedAssignment
def __init__(self, language, series, season, episode, id, rip, release):
super(TVsubtitlesSubtitle, self).__init__(language)
self.series = series
self.season = season
@@ -59,11 +59,10 @@ class TVsubtitlesProvider(Provider):
languages = {babelfish.Language('por', 'BR')} | {babelfish.Language(l)
for l in ['ara', 'bul', 'ces', 'dan', 'deu', 'ell', 'eng', 'fin', 'fra', 'hun', 'ita', 'jpn', 'kor',
'nld', 'pol', 'por', 'ron', 'rus', 'spa', 'swe', 'tur', 'ukr', 'zho']}
video_types = (Episode,)
videos = (Episode,)
server = 'http://www.tvsubtitles.net'
episode_id_re = re.compile('^episode-\d+\.html$')
subtitle_re = re.compile('^\/subtitle-\d+\.html$')
link_re = re.compile('^(?P<series>.+) \(\d{4}-\d{4}\)$')
episode_id_re = re.compile('^episode-(\d+)\.html$')
subtitle_re = re.compile('^\/subtitle-(\d+)\.html$')
def initialize(self):
self.session = requests.Session()
@@ -78,7 +77,6 @@ class TVsubtitlesProvider(Provider):
:param string url: part of the URL to reach with the leading slash
:param dict params: params of the request
:param dict data: data of the request
:param string method: method of the request
:return: the response
:rtype: :class:`bs4.BeautifulSoup`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
@@ -108,13 +106,6 @@ class TVsubtitlesProvider(Provider):
if not links:
logger.info('Series %r not found', series)
return None
for link in links:
match = self.link_re.match(link.string)
if not match:
logger.warning('Could not parse %r', link.string)
continue
if match.group('series').lower().replace('.', ' ').strip() == series.lower():
return int(link['href'][8:-5])
return int(links[0]['href'][8:-5])
@region.cache_on_arguments()
@@ -169,7 +160,7 @@ class TVsubtitlesProvider(Provider):
if len(zf.namelist()) > 1:
raise ProviderError('More than one file to unzip')
subtitle_bytes = zf.read(zf.namelist()[0])
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'], 'replace')
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'])
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+1 -1
View File
@@ -6,7 +6,7 @@ from sympy import Eq, symbols, solve
# Symbols
release_group, resolution, video_codec, audio_codec = symbols('release_group resolution video_codec audio_codec')
imdb_id, hash, title, series, tvdb_id, season, episode = symbols('imdb_id hash title series tvdb_id season episode') # @ReservedAssignment
imdb_id, hash, title, series, tvdb_id, season, episode = symbols('imdb_id hash title series tvdb_id season episode')
year = symbols('year')
+4 -8
View File
@@ -73,7 +73,7 @@ class Subtitle(object):
return score
def __repr__(self):
return '<%s [%s]>' % (self.__class__.__name__, self.language)
return '<%s [%r]>' % (self.__class__.__name__, self.language)
def get_subtitle_path(video_path, language=None):
@@ -90,7 +90,7 @@ def get_subtitle_path(video_path, language=None):
if language is not None:
try:
return subtitle_path + '.%s.%s' % (language.alpha2, 'srt')
except babelfish.LanguageConvertError:
except babelfish.NoConversionError:
return subtitle_path + '.%s.%s' % (language.alpha3, 'srt')
return subtitle_path + '.srt'
@@ -105,12 +105,8 @@ def is_valid_subtitle(subtitle_text):
try:
pysrt.from_string(subtitle_text, error_handling=pysrt.ERROR_RAISE)
return True
except pysrt.Error as e:
if e.args[0] > 80:
return True
except:
logger.exception('Unexpected error when validating subtitle')
return False
except pysrt.Error:
return False
def compute_guess_matches(video, guess):
+1 -1
View File
@@ -6,7 +6,7 @@ from subliminal import cache_region
from . import test_providers, test_subliminal
cache_region.configure('dogpile.cache.memory', expiration_time=60 * 30) # @UndefinedVariable
cache_region.configure('dogpile.cache.memory', expiration_time=60 * 30)
suite = TestSuite([test_providers.suite(), test_subliminal.suite()])
+61 -137
View File
@@ -25,18 +25,17 @@ class Addic7edProviderTestCase(ProviderTestCase):
def test_find_show_id(self):
with self.Provider() as provider:
show_id = provider.find_show_id('The Big Bang')
self.assertEqual(show_id, 126)
self.assertTrue(show_id == 126)
def test_find_show_id_error(self):
with self.Provider() as provider:
show_id = provider.find_show_id('the big how i met your mother')
self.assertIsNone(show_id)
self.assertTrue(show_id is None)
def test_get_show_ids(self):
with self.Provider() as provider:
show_ids = provider.get_show_ids()
self.assertIn('the big bang theory', show_ids)
self.assertEqual(show_ids['the big bang theory'], 126)
self.assertTrue('the big bang theory' in show_ids and show_ids['the big bang theory'] == 126)
def test_query_episode_0(self):
video = EPISODES[0]
@@ -51,8 +50,8 @@ class Addic7edProviderTestCase(ProviderTestCase):
frozenset(['series', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1(self):
video = EPISODES[1]
@@ -69,8 +68,8 @@ class Addic7edProviderTestCase(ProviderTestCase):
frozenset(['series', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = EPISODES[0]
@@ -79,8 +78,8 @@ class Addic7edProviderTestCase(ProviderTestCase):
frozenset(['series', 'episode', 'season', 'title'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = EPISODES[0]
@@ -94,16 +93,6 @@ class Addic7edProviderTestCase(ProviderTestCase):
class BierDopjeProviderTestCase(ProviderTestCase):
provider_name = 'bierdopje'
def test_find_show_id(self):
with self.Provider() as provider:
show_id = provider.find_show_id('The Big Bang')
self.assertEqual(show_id, 9203)
def test_find_show_id_error(self):
with self.Provider() as provider:
show_id = provider.find_show_id('the big how i met your mother')
self.assertIsNone(show_id)
def test_query_episode_0(self):
video = EPISODES[0]
language = Language('eng')
@@ -112,8 +101,8 @@ class BierDopjeProviderTestCase(ProviderTestCase):
frozenset(['episode', 'video_codec', 'season', 'series', 'resolution', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.query(language, video.season, video.episode, series=video.series)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, {language})
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_query_episode_1(self):
video = EPISODES[1]
@@ -125,8 +114,8 @@ class BierDopjeProviderTestCase(ProviderTestCase):
frozenset(['episode', 'video_codec', 'season', 'series', 'resolution', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.query(language, video.season, video.episode, series=video.series)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, {language})
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_query_episode_0_tvdb_id(self):
video = EPISODES[0]
@@ -136,8 +125,8 @@ class BierDopjeProviderTestCase(ProviderTestCase):
frozenset(['episode', 'series', 'video_codec', 'tvdb_id', 'resolution', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(language, video.season, video.episode, tvdb_id=video.tvdb_id)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, {language})
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_list_subtitles(self):
video = EPISODES[1]
@@ -149,8 +138,8 @@ class BierDopjeProviderTestCase(ProviderTestCase):
frozenset(['episode', 'video_codec', 'season', 'series', 'tvdb_id', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = EPISODES[0]
@@ -174,8 +163,8 @@ class OpenSubtitlesProviderTestCase(ProviderTestCase):
frozenset(['imdb_id', 'title', 'year', 'video_codec', 'resolution', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.query(languages, query=video.title)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_0_query(self):
video = EPISODES[0]
@@ -185,8 +174,8 @@ class OpenSubtitlesProviderTestCase(ProviderTestCase):
frozenset(['episode', 'title', 'series', 'imdb_id', 'video_codec', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(languages, query=video.name.split(os.sep)[-1])
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1_query(self):
video = EPISODES[1]
@@ -199,8 +188,8 @@ class OpenSubtitlesProviderTestCase(ProviderTestCase):
frozenset(['series', 'episode', 'season', 'imdb_id'])}
with self.Provider() as provider:
subtitles = provider.query(languages, query=video.name.split(os.sep)[-1])
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_movie_0_imdb_id(self):
video = MOVIES[0]
@@ -212,8 +201,8 @@ class OpenSubtitlesProviderTestCase(ProviderTestCase):
frozenset(['imdb_id', 'resolution', 'title', 'year'])}
with self.Provider() as provider:
subtitles = provider.query(languages, imdb_id=video.imdb_id)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_0_imdb_id(self):
video = EPISODES[0]
@@ -224,22 +213,20 @@ class OpenSubtitlesProviderTestCase(ProviderTestCase):
frozenset(['episode', 'title', 'series', 'imdb_id', 'video_codec', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(languages, imdb_id=video.imdb_id)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_movie_0_hash(self):
video = MOVIES[0]
languages = {Language('eng')}
matches = {frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'imdb_id']),
frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'release_group', 'imdb_id']),
frozenset(['year', 'video_codec', 'imdb_id', 'hash', 'title']),
frozenset([]),
frozenset(['year', 'resolution', 'imdb_id', 'hash', 'title']),
frozenset(['year', 'imdb_id', 'hash', 'title'])}
matches = {frozenset(['imdb_id', 'title', 'hash', 'year']),
frozenset(['imdb_id', 'hash', 'title', 'year', 'video_codec', 'resolution']),
frozenset(['imdb_id', 'video_codec', 'hash', 'title', 'year']),
frozenset(['imdb_id', 'hash', 'title', 'year', 'video_codec', 'resolution', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.query(languages, hash=video.hashes['opensubtitles'], size=video.size)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_0_hash(self):
video = EPISODES[0]
@@ -250,77 +237,25 @@ class OpenSubtitlesProviderTestCase(ProviderTestCase):
frozenset(['series', 'resolution', 'hash', 'video_codec'])}
with self.Provider() as provider:
subtitles = provider.query(languages, hash=video.hashes['opensubtitles'], size=video.size)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['title', 'video_codec', 'year', 'resolution', 'release_group', 'imdb_id']),
frozenset(['imdb_id', 'year', 'title']),
frozenset(['year', 'video_codec', 'imdb_id', 'resolution', 'title']),
frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'release_group', 'imdb_id']),
frozenset(['year', 'video_codec', 'imdb_id', 'hash', 'title']),
frozenset([]),
frozenset(['year', 'resolution', 'imdb_id', 'hash', 'title']),
frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'imdb_id']),
frozenset(['year', 'imdb_id', 'hash', 'title']),
frozenset(['video_codec', 'imdb_id', 'year', 'title']),
frozenset(['year', 'imdb_id', 'resolution', 'title'])}
matches = {frozenset(['imdb_id', 'title', 'hash', 'year']),
frozenset(['imdb_id', 'resolution', 'title', 'year']),
frozenset(['imdb_id', 'title', 'year']),
frozenset(['imdb_id', 'video_codec', 'title', 'year']),
frozenset(['imdb_id', 'resolution', 'title', 'video_codec', 'year']),
frozenset(['imdb_id', 'hash', 'title', 'year', 'video_codec', 'resolution', 'release_group']),
frozenset(['imdb_id', 'video_codec', 'hash', 'title', 'year']),
frozenset(['imdb_id', 'title', 'year', 'video_codec', 'resolution', 'release_group']),
frozenset(['imdb_id', 'hash', 'title', 'year', 'video_codec', 'resolution'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
def test_download_subtitle(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class PodnapisiProviderTestCase(ProviderTestCase):
provider_name = 'podnapisi'
def test_query_movie_0(self):
video = MOVIES[0]
language = Language('eng')
matches = {frozenset(['video_codec', 'title', 'resolution', 'year']),
frozenset(['title', 'resolution', 'year']),
frozenset(['video_codec', 'title', 'year']),
frozenset(['title', 'year']),
frozenset(['video_codec', 'title', 'resolution', 'release_group', 'year']),
frozenset(['video_codec', 'title', 'resolution', 'audio_codec', 'year'])}
with self.Provider() as provider:
subtitles = provider.query(language, title=video.title, year=video.year)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, {language})
def test_query_episode_0(self):
video = EPISODES[0]
language = Language('eng')
matches = {frozenset(['episode', 'series', 'season', 'video_codec', 'resolution', 'release_group']),
frozenset(['season', 'video_codec', 'episode', 'resolution', 'series'])}
with self.Provider() as provider:
subtitles = provider.query(language, series=video.series, season=video.season, episode=video.episode)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, {language})
def test_list_subtitles(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['video_codec', 'title', 'resolution', 'year']),
frozenset(['title', 'resolution', 'year']),
frozenset(['video_codec', 'title', 'year']),
frozenset(['title', 'year']),
frozenset(['video_codec', 'title', 'resolution', 'release_group', 'year']),
frozenset(['video_codec', 'title', 'resolution', 'audio_codec', 'year'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = MOVIES[0]
@@ -340,8 +275,8 @@ class TheSubDBProviderTestCase(ProviderTestCase):
matches = {frozenset(['hash'])}
with self.Provider() as provider:
subtitles = provider.query(video.hashes['thesubdb'])
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1(self):
video = EPISODES[1]
@@ -349,8 +284,8 @@ class TheSubDBProviderTestCase(ProviderTestCase):
matches = {frozenset(['hash'])}
with self.Provider() as provider:
subtitles = provider.query(video.hashes['thesubdb'])
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = MOVIES[0]
@@ -358,8 +293,8 @@ class TheSubDBProviderTestCase(ProviderTestCase):
matches = {frozenset(['hash'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = MOVIES[0]
@@ -376,27 +311,17 @@ class TVsubtitlesProviderTestCase(ProviderTestCase):
def test_find_show_id(self):
with self.Provider() as provider:
show_id = provider.find_show_id('The Big Bang')
self.assertEqual(show_id, 154)
def test_find_show_id_ambiguous(self):
with self.Provider() as provider:
show_id = provider.find_show_id('New Girl')
self.assertEqual(show_id, 977)
def test_find_show_id_no_dots(self):
with self.Provider() as provider:
show_id = provider.find_show_id('Marvel\'s Agents of S H I E L D')
self.assertEqual(show_id, 1340)
self.assertTrue(show_id == 154)
def test_find_show_id_error(self):
with self.Provider() as provider:
show_id = provider.find_show_id('the big gaming')
self.assertIsNone(show_id)
self.assertTrue(show_id is None)
def test_find_episode_ids(self):
with self.Provider() as provider:
episode_ids = provider.find_episode_ids(154, 5)
self.assertEqual(set(episode_ids.keys()), set(range(1, 25)))
self.assertTrue(set(episode_ids.keys()) == set(range(1, 25)))
def test_query_episode_0(self):
video = EPISODES[0]
@@ -405,8 +330,8 @@ class TVsubtitlesProviderTestCase(ProviderTestCase):
frozenset(['series', 'episode', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season, video.episode)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1(self):
video = EPISODES[1]
@@ -417,8 +342,8 @@ class TVsubtitlesProviderTestCase(ProviderTestCase):
frozenset(['series', 'episode', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season, video.episode)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = EPISODES[0]
@@ -426,8 +351,8 @@ class TVsubtitlesProviderTestCase(ProviderTestCase):
matches = {frozenset(['series', 'episode', 'season'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
self.assertEqual({subtitle.language for subtitle in subtitles}, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = EPISODES[0]
@@ -443,7 +368,6 @@ def suite():
suite.addTest(TestLoader().loadTestsFromTestCase(Addic7edProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(BierDopjeProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(OpenSubtitlesProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(PodnapisiProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(TheSubDBProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(TVsubtitlesProviderTestCase))
return suite
+11 -76
View File
@@ -5,7 +5,7 @@ import os
import shutil
from unittest import TestCase, TestSuite, TestLoader, TextTestRunner
from babelfish import Language
from subliminal import list_subtitles, download_subtitles, download_best_subtitles, scan_video
from subliminal import list_subtitles, download_subtitles, download_best_subtitles
from subliminal.tests.common import MOVIES, EPISODES
@@ -23,22 +23,22 @@ class ApiTestCase(TestCase):
videos = [MOVIES[0]]
languages = {Language('eng')}
subtitles = list_subtitles(videos, languages)
self.assertEqual(len(subtitles), len(videos))
self.assertGreater(len(subtitles[videos[0]]), 0)
self.assertTrue(len(subtitles) == len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_list_subtitles_movie_0_por_br(self):
videos = [MOVIES[0]]
languages = {Language('por', 'BR')}
subtitles = list_subtitles(videos, languages)
self.assertEqual(len(subtitles), len(videos))
self.assertGreater(len(subtitles[videos[0]]), 0)
self.assertTrue(len(subtitles) == len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_list_subtitles_episodes(self):
videos = [EPISODES[0], EPISODES[1]]
languages = {Language('eng'), Language('fra')}
subtitles = list_subtitles(videos, languages)
self.assertEqual(len(subtitles), len(videos))
self.assertGreater(len(subtitles[videos[0]]), 0)
self.assertTrue(len(subtitles) == len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_download_subtitles(self):
videos = [EPISODES[0], EPISODES[1]]
@@ -68,7 +68,7 @@ class ApiTestCase(TestCase):
languages = {Language('eng'), Language('fra')}
subtitles = download_best_subtitles(videos, languages)
for video in videos:
self.assertEqual(video in subtitles and len(subtitles[video]), 2)
self.assertTrue(video in subtitles and len(subtitles[video]) == 2)
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.en.srt'))
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.fr.srt'))
@@ -79,8 +79,7 @@ class ApiTestCase(TestCase):
languages = {Language('eng'), Language('fra')}
subtitles = download_best_subtitles(videos, languages, single=True)
for video in videos:
self.assertIn(video, subtitles)
self.assertEqual(len(subtitles[video]), 1)
self.assertTrue(video in subtitles and len(subtitles[video]) == 1)
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.srt'))
def test_download_best_subtitles_min_score(self):
@@ -89,7 +88,7 @@ class ApiTestCase(TestCase):
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = download_best_subtitles(videos, languages, min_score=1000)
self.assertEqual(len(subtitles), 0)
self.assertTrue(len(subtitles) == 0)
def test_download_best_subtitles_hearing_impaired(self):
videos = [MOVIES[0]]
@@ -97,76 +96,12 @@ class ApiTestCase(TestCase):
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng')}
subtitles = download_best_subtitles(videos, languages, hearing_impaired=True)
self.assertTrue(subtitles[videos[0]][0].hearing_impaired)
class VideoTestCase(TestCase):
def setUp(self):
os.mkdir(TEST_DIR)
for video in MOVIES + EPISODES:
open(os.path.join(TEST_DIR, os.path.split(video.name)[1]), 'w').close()
def tearDown(self):
shutil.rmtree(TEST_DIR)
def test_scan_video_movie(self):
video = MOVIES[0]
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertEqual(scanned_video.name, os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertEqual(scanned_video.title.lower(), video.title.lower())
self.assertEqual(scanned_video.year, video.year)
self.assertEqual(scanned_video.video_codec, video.video_codec)
self.assertEqual(scanned_video.resolution, video.resolution)
self.assertEqual(scanned_video.release_group, video.release_group)
self.assertEqual(scanned_video.subtitle_languages, set())
self.assertEqual(scanned_video.hashes, {})
self.assertIsNone(scanned_video.audio_codec)
self.assertIsNone(scanned_video.imdb_id)
self.assertEqual(scanned_video.size, 0)
def test_scan_video_episode(self):
video = EPISODES[0]
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertEqual(scanned_video.name, os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertEqual(scanned_video.series, video.series)
self.assertEqual(scanned_video.season, video.season)
self.assertEqual(scanned_video.episode, video.episode)
self.assertEqual(scanned_video.video_codec, video.video_codec)
self.assertEqual(scanned_video.resolution, video.resolution)
self.assertEqual(scanned_video.release_group, video.release_group)
self.assertEqual(scanned_video.subtitle_languages, set())
self.assertEqual(scanned_video.hashes, {})
self.assertIsNone(scanned_video.title)
self.assertIsNone(scanned_video.tvdb_id)
self.assertIsNone(scanned_video.imdb_id)
self.assertIsNone(scanned_video.audio_codec)
self.assertEqual(scanned_video.size, 0)
def test_scan_video_subtitle_language_und(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertEqual(scanned_video.subtitle_languages, {Language('und')})
def test_scan_video_subtitles_language_eng(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.en.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertEqual(scanned_video.subtitle_languages, {Language('eng')})
def test_scan_video_subtitles_languages(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.en.srt', 'w').close()
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.fr.srt', 'w').close()
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertEqual(scanned_video.subtitle_languages, {Language('eng'), Language('fra'), Language('und')})
self.assertTrue(subtitles[videos[0]][0].hearing_impaired == True)
def suite():
suite = TestSuite()
suite.addTest(TestLoader().loadTestsFromTestCase(ApiTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(VideoTestCase))
return suite
+66 -122
View File
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import datetime
import hashlib
import logging
import os
@@ -24,6 +23,9 @@ VIDEO_EXTENSIONS = ('.3g2', '.3gp', '.3gp2', '.3gpp', '.60d', '.ajp', '.asf', '.
#: Subtitle extensions
SUBTITLE_EXTENSIONS = ('.srt', '.sub', '.smi', '.txt', '.ssa', '.ass', '.mpl')
#: Language extensions
LANGUAGE_EXTENSIONS = tuple('.' + c for c in babelfish.CONVERTERS['alpha2'].codes)
class Video(object):
"""Base class for videos
@@ -156,15 +158,10 @@ def scan_subtitle_languages(path):
:rtype: set
"""
language_extensions = tuple('.' + c for c in babelfish.get_language_converter('alpha2').codes)
dirpath, filename = os.path.split(path)
subtitles = set()
for p in os.listdir(dirpath):
if not isinstance(p, bytes) and p.startswith(os.path.splitext(filename)[0]) and p.endswith(SUBTITLE_EXTENSIONS):
if os.path.splitext(p)[0].endswith(language_extensions):
subtitles.add(babelfish.Language.fromalpha2(os.path.splitext(p)[0][-2:]))
else:
subtitles.add(babelfish.Language('und'))
subtitles = {babelfish.Language.fromalpha2(os.path.splitext(p)[0][-2:]) for p in os.listdir(dirpath)
if not isinstance(p, bytes) and p.startswith(os.path.splitext(filename)[0])
and os.path.splitext(p)[0].endswith(LANGUAGE_EXTENSIONS)}
logger.debug('Found subtitles %r', subtitles)
return subtitles
@@ -172,7 +169,7 @@ def scan_subtitle_languages(path):
def scan_video(path, subtitles=True, embedded_subtitles=True):
"""Scan a video and its subtitle languages from a video `path`
:param string path: absolute path to the video
:param string path: path to the video
:param bool subtitles: scan for subtitles with the same name
:param bool embedded_subtitles: scan for embedded subtitles
:return: the scanned video
@@ -182,90 +179,63 @@ def scan_video(path, subtitles=True, embedded_subtitles=True):
"""
dirpath, filename = os.path.split(path)
logger.info('Scanning video %r in %r', filename, dirpath)
video = Video.fromguess(path, guessit.guess_file_info(path, 'autodetect'))
video = Video.fromguess(path, guessit.guess_file_info(filename, 'autodetect'))
# mkv container
if filename.endswith('.mkv'):
with open(path, 'rb') as f:
mkv = enzyme.MKV(f)
video_track = mkv.video_tracks[0]
audio_track = mkv.audio_tracks[0]
# resolution
if video_track.height in (480, 720, 1080):
if video_track.interlaced:
video.resolution = '%di' % video_track.height
logger.debug('Found resolution %s with enzyme', video.resolution)
else:
video.resolution = '%dp' % video_track.height
logger.debug('Found resolution %s with enzyme', video.resolution)
# video codec
if video_track.codec_id == 'V_MPEG4/ISO/AVC':
video.video_codec = 'h264'
logger.debug('Found video_codec %s with enzyme', video.video_codec)
elif video_track.codec_id == 'V_MPEG4/ISO/SP':
video.video_codec = 'DivX'
logger.debug('Found video_codec %s with enzyme', video.video_codec)
elif video_track.codec_id == 'V_MPEG4/ISO/ASP':
video.video_codec = 'XviD'
logger.debug('Found video_codec %s with enzyme', video.video_codec)
# audio codec
if audio_track.codec_id == 'A_AC3':
video.audio_codec = 'AC3'
logger.debug('Found audio_codec %s with enzyme', video.audio_codec)
elif audio_track.codec_id == 'A_DTS':
video.audio_codec = 'DTS'
logger.debug('Found audio_codec %s with enzyme', video.audio_codec)
elif audio_track.codec_id == 'A_AAC':
video.audio_codec = 'AAC'
logger.debug('Found audio_codec %s with enzyme', video.audio_codec)
# embedded subtitles
if embedded_subtitles:
embedded_subtitle_languages = {babelfish.Language.fromalpha3b(st.language) for st in
mkv.subtitle_tracks if st.language != 'und'}
if embedded_subtitle_languages:
logger.debug('Found embedded subtitle %r with enzyme', embedded_subtitle_languages)
video.subtitle_languages |= embedded_subtitle_languages
video.size = os.path.getsize(path)
if video.size > 10485760:
logger.debug('Size is %d', video.size)
video.hashes['opensubtitles'] = hash_opensubtitles(path)
video.hashes['thesubdb'] = hash_thesubdb(path)
logger.debug('Computed hashes %r', video.hashes)
else:
logger.warning('Size is lower than 10MB: hashes not computed')
logger.debug('Size is %d', video.size)
video.hashes['opensubtitles'] = hash_opensubtitles(path)
video.hashes['thesubdb'] = hash_thesubdb(path)
logger.debug('Computed hashes %r', video.hashes)
# add subtitles
if subtitles:
video.subtitle_languages |= scan_subtitle_languages(path)
# enzyme
try:
if filename.endswith('.mkv'):
with open(path, 'rb') as f:
mkv = enzyme.MKV(f)
if mkv.video_tracks:
video_track = mkv.video_tracks[0]
# resolution
if video_track.height in (480, 720, 1080):
if video_track.interlaced:
video.resolution = '%di' % video_track.height
logger.debug('Found resolution %s with enzyme', video.resolution)
else:
video.resolution = '%dp' % video_track.height
logger.debug('Found resolution %s with enzyme', video.resolution)
# video codec
if video_track.codec_id == 'V_MPEG4/ISO/AVC':
video.video_codec = 'h264'
logger.debug('Found video_codec %s with enzyme', video.video_codec)
elif video_track.codec_id == 'V_MPEG4/ISO/SP':
video.video_codec = 'DivX'
logger.debug('Found video_codec %s with enzyme', video.video_codec)
elif video_track.codec_id == 'V_MPEG4/ISO/ASP':
video.video_codec = 'XviD'
logger.debug('Found video_codec %s with enzyme', video.video_codec)
else:
logger.warning('MKV has no video track')
if mkv.audio_tracks:
audio_track = mkv.audio_tracks[0]
# audio codec
if audio_track.codec_id == 'A_AC3':
video.audio_codec = 'AC3'
logger.debug('Found audio_codec %s with enzyme', video.audio_codec)
elif audio_track.codec_id == 'A_DTS':
video.audio_codec = 'DTS'
logger.debug('Found audio_codec %s with enzyme', video.audio_codec)
elif audio_track.codec_id == 'A_AAC':
video.audio_codec = 'AAC'
logger.debug('Found audio_codec %s with enzyme', video.audio_codec)
else:
logger.warning('MKV has no audio track')
if mkv.subtitle_tracks:
# embedded subtitles
if embedded_subtitles:
embedded_subtitle_languages = set()
for st in mkv.subtitle_tracks:
if st.language:
try:
embedded_subtitle_languages.add(babelfish.Language.fromalpha3b(st.language))
except babelfish.Error:
logger.error('Embedded subtitle track language %r is not a valid language', st.language)
embedded_subtitle_languages.add(babelfish.Language('und'))
elif st.name:
try:
embedded_subtitle_languages.add(babelfish.Language.fromname(st.name))
except babelfish.Error:
logger.error('Embedded subtitle track name %r is not a valid language', st.name)
embedded_subtitle_languages.add(babelfish.Language('und'))
else:
embedded_subtitle_languages.add(babelfish.Language('und'))
logger.debug('Found embedded subtitle %r with enzyme', embedded_subtitle_languages)
video.subtitle_languages |= embedded_subtitle_languages
else:
logger.debug('MKV has no subtitle track')
except enzyme.Error:
logger.error('Parsing video metadata with enzyme failed')
return video
def scan_videos(paths, subtitles=True, embedded_subtitles=True, age=None):
"""Scan `paths` for videos and their subtitle languages
:params paths: absolute paths to scan for videos
:params paths: paths to scan for videos
:type paths: list of string
:param bool subtitles: scan for subtitles with the same name
:param bool embedded_subtitles: scan for embedded subtitles
@@ -278,57 +248,31 @@ def scan_videos(paths, subtitles=True, embedded_subtitles=True, age=None):
videos = []
# scan files
for filepath in [p for p in paths if os.path.isfile(p)]:
if age and datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(filepath)) > age:
logger.info('Skipping video %r: older than %r', filepath, age)
continue
try:
videos.append(scan_video(filepath, subtitles, embedded_subtitles))
videos.append(scan_video(filepath, subtitles))
except ValueError as e:
logger.error('Skipping video: %s', e)
logger.info('Skipping video: %s', e)
continue
# scan directories
for path in [p for p in paths if os.path.isdir(p)]:
logger.info('Scanning directory %r', path)
for dirpath, dirnames, filenames in os.walk(path):
# skip badly encoded directories
for dirpath, _, filenames in os.walk(path):
# skip badly encoded directories and files
if isinstance(dirpath, bytes):
logger.error('Skipping badly encoded directory %r', dirpath.decode('utf-8', errors='replace'))
continue
# skip badly encoded and hidden sub directories
for dirname in list(dirnames):
if isinstance(dirname, bytes):
logger.error('Skipping badly encoded dirname %r in %r', dirname.decode('utf-8', errors='replace'),
dirpath)
dirnames.remove(dirname)
elif dirname.startswith('.'):
logger.debug('Skipping hidden dirname %r in %r', dirname, dirpath)
dirnames.remove(dirname)
# scan for videos
safe_filenames = []
for filename in filenames:
# skip badly encoded files
if isinstance(filename, bytes):
logger.error('Skipping badly encoded filename %r in %r', filename.decode('utf-8', errors='replace'),
dirpath)
continue
# filter videos
if not filename.endswith(VIDEO_EXTENSIONS):
continue
# skip hidden files
if filename.startswith('.'):
logger.debug('Skipping hidden filename %r in %r', filename, dirpath)
continue
filepath = os.path.join(dirpath, filename)
# skip links
if os.path.islink(filepath):
logger.debug('Skipping link %r in %r', filename, dirpath)
continue
if age and datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(filepath)) > age:
logger.info('Skipping video %r: older than %r', filepath, age)
logger.error('Skipping badly encoded filename %r', filename.decode('utf-8', errors='replace'))
continue
safe_filenames.append(filename)
# scan for videos
for video_filename in [f for f in safe_filenames if f.endswith(VIDEO_EXTENSIONS)]:
try:
video = scan_video(filepath, subtitles, embedded_subtitles)
video = scan_video(os.path.join(dirpath, video_filename), subtitles=subtitles)
except ValueError as e:
logger.error('Skipping video: %s', e)
logger.info('Skipping video: %s', e)
continue
videos.append(video)
return videos