diff --git a/.build/patches/subliminal.patch b/.build/patches/subliminal.patch new file mode 100644 index 0000000000000000000000000000000000000000..b89b2a9ac330da6427ec3515ab8648c2a0f8f182 --- /dev/null +++ b/.build/patches/subliminal.patch @@ -0,0 +1,151 @@ +diff --git a/sublimnal/api.py b/subliminal/api.py +index 13c614f..6fe3368 100644 +--- a/sublimnal/api.py ++++ b/subliminal/api.py +@@ -7,6 +7,7 @@ import os.path + import socket + + from babelfish import Language ++from pkg_resources import EntryPoint + import requests + from stevedore import EnabledExtensionManager, ExtensionManager + +@@ -14,7 +15,30 @@ from .subtitle import compute_score, get_subtitle_path + + logger = logging.getLogger(__name__) + +-provider_manager = ExtensionManager('subliminal.providers') ++ ++class InternalExtensionManager(ExtensionManager): ++ """Add support for internal entry points to the :class:`~stevedore.extension.Extensionmanager` ++ Internal entry points are useful for libraries that ship their own plugins but still keep the entry point open. ++ All other parameters are passed onwards to the :class:`~stevedore.extension.Extensionmanager` constructor. ++ :param internal_entry_points: the internal providers ++ :type internal_entry_points: list of :class:`~pkg_resources.EntryPoint` ++ """ ++ def __init__(self, namespace, internal_entry_points, **kwargs): ++ self.internal_entry_points = list(internal_entry_points) ++ super(InternalExtensionManager, self).__init__(namespace, **kwargs) ++ ++ def _find_entry_points(self, namespace): ++ return self.internal_entry_points + super(InternalExtensionManager, self)._find_entry_points(namespace) ++ ++ ++provider_manager = InternalExtensionManager('subliminal.providers', [EntryPoint.parse(ep) for ep in ( ++ 'addic7ed = subliminal.providers.addic7ed:Addic7edProvider', ++ 'napiprojekt = subliminal.providers.napiprojekt:NapiProjektProvider', ++ 'opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider', ++ 'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider', ++ 'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider', ++ 'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider' ++)]) + + + class ProviderPool(object): +diff --git a/subliminal/compat.py b/subliminal/compat.py +new file mode 100644 +index 0000000..28bd3e8 +--- /dev/null ++++ b/subliminal/compat.py +@@ -0,0 +1,21 @@ ++# -*- coding: utf-8 -*- ++import sys ++import socket ++ ++ ++if sys.version_info[0] == 2: ++ from xmlrpclib import ServerProxy, Transport ++ from httplib import HTTPConnection ++elif sys.version_info[0] == 3: ++ from xmlrpc.client import ServerProxy, Transport ++ from http.client import HTTPConnection ++ ++ ++class TimeoutTransport(Transport, object): ++ def __init__(self, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, *args, **kwargs): ++ super(TimeoutTransport, self).__init__(*args, **kwargs) ++ self.timeout = timeout ++ ++ def make_connection(self, host): ++ h = HTTPConnection(host, timeout=self.timeout) ++ return h +diff --git a/subliminal/converters/podnapisi.py b/subliminal/converters/podnapisi.py +new file mode 100644 +index 0000000..d73cb1c +--- /dev/null ++++ b/subliminal/converters/podnapisi.py +@@ -0,0 +1,32 @@ ++# -*- 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] +diff --git a/sublimnal/providers/addic7ed.py b/subliminal/providers/addic7ed.py +index edaa728..3edfe35 100644 +--- a/sublimnal/providers/addic7ed.py ++++ b/subliminal/providers/addic7ed.py +@@ -2,7 +2,7 @@ + import logging + import re + +-from babelfish import Language ++from babelfish import Language, language_converters + from requests import Session + + from . import ParserBeautifulSoup, Provider, get_version +@@ -13,6 +13,7 @@ from ..subtitle import Subtitle, fix_line_ending, guess_matches, guess_propertie + from ..video import Episode + + logger = logging.getLogger(__name__) ++language_converters.register('addic7ed = subliminal.converters.addic7ed:Addic7edConverter') + + series_year_re = re.compile('^(?P<series>[ \w]+)(?: \((?P<year>\d{4})\))?$') + +diff --git a/sublimnal/providers/tvsubtitles.py b/subliminal/providers/tvsubtitles.py +index e63cd43..06fd878 100644 +--- a/sublimnal/providers/tvsubtitles.py ++++ b/subliminal/providers/tvsubtitles.py +@@ -4,7 +4,7 @@ import logging + import re + from zipfile import ZipFile + +-from babelfish import Language ++from babelfish import Language, language_converters + from requests import Session + + from . import ParserBeautifulSoup, Provider, get_version +@@ -15,6 +15,7 @@ from ..subtitle import Subtitle, fix_line_ending, guess_matches, guess_propertie + from ..video import Episode + + logger = logging.getLogger(__name__) ++language_converters.register('tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter') + + link_re = re.compile('^(?P<series>.+?)(?: \(?\d{4}\)?| \((?:US|UK)\))? \((?P<first_year>\d{4})-\d{4}\)$') + episode_id_re = re.compile('^episode-\d+\.html$') diff --git a/gui/slick/images/subtitles/napiprojekt.png b/gui/slick/images/subtitles/napiprojekt.png new file mode 100644 index 0000000000000000000000000000000000000000..147c6658ad74434a41b1d46744dddb3659df330e Binary files /dev/null and b/gui/slick/images/subtitles/napiprojekt.png differ diff --git a/gui/slick/views/config_providers.mako b/gui/slick/views/config_providers.mako index 365951b34703c320827b0b3f98ae46a4aeb695f0..25186301c5f5a343a82858e879e52d3637461f22 100644 --- a/gui/slick/views/config_providers.mako +++ b/gui/slick/views/config_providers.mako @@ -432,6 +432,18 @@ $('#config-components').tabs(); </div> % endif + % if hasattr(curTorrentProvider, 'engrelease'): + <div class="field-pair"> + <label for="${curTorrentProvider.getID()}_engrelease"> + <span class="component-title">English torrents</span> + <span class="component-desc"> + <input type="checkbox" name="${curTorrentProvider.getID()}_engrelease" id="${curTorrentProvider.getID()}_engrelease" ${('', 'checked="checked"')[bool(curTorrentProvider.engrelease)]} /> + <p>only download english torrents ,or torrents containing english subtitles</p> + </span> + </label> + </div> + % endif + % if hasattr(curTorrentProvider, 'sorting'): <div class="field-pair"> <label for="${curTorrentProvider.getID()}_sorting"> diff --git a/lib/subliminal/__init__.py b/lib/subliminal/__init__.py index 68de539bffcb3e3ecac2062f79addbe0c58e2474..23dd54d0ecd1c0414a204f199b6d1670da5a38eb 100644 --- a/lib/subliminal/__init__.py +++ b/lib/subliminal/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- __title__ = 'subliminal' -__version__ = '1.0.dev0' +__version__ = '1.1.0.dev0' __author__ = 'Antoine Bertin' __license__ = 'MIT' __copyright__ = 'Copyright 2015, Antoine Bertin' @@ -12,7 +12,7 @@ from .api import (ProviderPool, check_video, provider_manager, download_best_sub from .cache import region from .exceptions import Error, ProviderError from .providers import Provider -from .subtitle import Subtitle +from .subtitle import Subtitle, compute_score from .video import SUBTITLE_EXTENSIONS, VIDEO_EXTENSIONS, Episode, Movie, Video, scan_video, scan_videos logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/lib/subliminal/api.py b/lib/subliminal/api.py index 92d7b4b7f24ab01f2716a237ef71714a67991b08..6fe33686f6016e46179f9aa22cd42bd586f83382 100644 --- a/lib/subliminal/api.py +++ b/lib/subliminal/api.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals from collections import defaultdict import io import logging @@ -10,8 +9,7 @@ import socket from babelfish import Language from pkg_resources import EntryPoint import requests -from stevedore import ExtensionManager -from stevedore.dispatch import DispatchExtensionManager +from stevedore import EnabledExtensionManager, ExtensionManager from .subtitle import compute_score, get_subtitle_path @@ -20,14 +18,10 @@ logger = logging.getLogger(__name__) class InternalExtensionManager(ExtensionManager): """Add support for internal entry points to the :class:`~stevedore.extension.Extensionmanager` - Internal entry points are useful for libraries that ship their own plugins but still keep the entry point open. - All other parameters are passed onwards to the :class:`~stevedore.extension.Extensionmanager` constructor. - :param internal_entry_points: the internal providers :type internal_entry_points: list of :class:`~pkg_resources.EntryPoint` - """ def __init__(self, namespace, internal_entry_points, **kwargs): self.internal_entry_points = list(internal_entry_points) @@ -39,6 +33,7 @@ class InternalExtensionManager(ExtensionManager): provider_manager = InternalExtensionManager('subliminal.providers', [EntryPoint.parse(ep) for ep in ( 'addic7ed = subliminal.providers.addic7ed:Addic7edProvider', + 'napiprojekt = subliminal.providers.napiprojekt:NapiProjektProvider', 'opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider', 'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider', 'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider', @@ -74,8 +69,8 @@ class ProviderPool(object): #: Discarded providers self.discarded_providers = set() - #: Dedicated :data:`provider_manager` as :class:`~stevedore.dispatch.DispatchExtensionManager` - self.manager = DispatchExtensionManager(provider_manager.namespace, lambda e: e.name in self.providers) + #: Dedicated :data:`provider_manager` as :class:`~stevedore.enabled.EnabledExtensionManager` + self.manager = EnabledExtensionManager(provider_manager.namespace, lambda e: e.name in self.providers) def __enter__(self): return self @@ -217,7 +212,7 @@ class ProviderPool(object): for subtitle, score in scored_subtitles: # check score if score < min_score: - logger.info('Score %d is below min_score (%d)', (score, min_score)) + logger.info('Score %d is below min_score (%d)', score, min_score) break # check downloaded languages @@ -297,7 +292,6 @@ def list_subtitles(videos, languages, **kwargs): :type videos: set of :class:`~subliminal.video.Video` :param languages: languages to search for. :type languages: set of :class:`~babelfish.language.Language` - :param providers: name of providers to use, if not all. :return: found subtitles per video. :rtype: dict of :class:`~subliminal.video.Video` to list of :class:`~subliminal.subtitle.Subtitle` @@ -334,8 +328,6 @@ def download_subtitles(subtitles, **kwargs): :param subtitles: subtitles to download. :type subtitles: list of :class:`~subliminal.subtitle.Subtitle` - :param dict provider_configs: provider configuration as keyword arguments per provider name to pass when. - instanciating the :class:`~subliminal.providers.Provider`. """ with ProviderPool(**kwargs) as pool: @@ -356,7 +348,6 @@ def download_best_subtitles(videos, languages, min_score=0, hearing_impaired=Fal :type videos: set of :class:`~subliminal.video.Video` :param languages: languages to download. :type languages: set of :class:`~babelfish.language.Language` - :param providers: name of providers to use, if not all. :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. diff --git a/lib/subliminal/cli.py b/lib/subliminal/cli.py index c1e690b0d6cb7fd0c90448595edc1cc2bb4e6ca4..537c21c8a430da21c333ff87d16cead816df8898 100644 --- a/lib/subliminal/cli.py +++ b/lib/subliminal/cli.py @@ -3,9 +3,10 @@ Subliminal uses `click <http://click.pocoo.org>`_ to provide a powerful :abbr:`CLI (command-line interface)`. """ -from __future__ import unicode_literals +from __future__ import division from collections import defaultdict from datetime import timedelta +import json import logging import os import re @@ -14,11 +15,14 @@ from babelfish import Error as BabelfishError, Language import click from dogpile.cache.backends.file import AbstractFileLock from dogpile.core import ReadWriteMutex +from six.moves import configparser from subliminal import (Episode, Movie, ProviderPool, Video, __version__, check_video, provider_manager, region, save_subtitles, scan_video, scan_videos) from subliminal.subtitle import compute_score +logger = logging.getLogger(__name__) + class MutexLock(AbstractFileLock): """:class:`MutexLock` is a thread-based rw lock based on :class:`dogpile.core.ReadWriteMutex`.""" @@ -40,6 +44,115 @@ class MutexLock(AbstractFileLock): return self.mutex.release_write_lock() +class Config(object): + """A :class:`~configparser.SafeConfigParser` wrapper to store configuration. + + Interaction with the configuration is done with the properties. + + :param str path: path to the configuration file. + + """ + def __init__(self, path): + #: Path to the configuration file + self.path = path + + #: The underlying configuration object + self.config = configparser.SafeConfigParser() + self.config.add_section('general') + self.config.set('general', 'languages', json.dumps(['en'])) + self.config.set('general', 'providers', json.dumps(sorted([p.name for p in provider_manager]))) + self.config.set('general', 'single', str(0)) + self.config.set('general', 'embedded_subtitles', str(1)) + self.config.set('general', 'age', str(int(timedelta(weeks=2).total_seconds()))) + self.config.set('general', 'hearing_impaired', str(1)) + self.config.set('general', 'min_score', str(0)) + + def read(self): + """Read the configuration from :attr:`path`""" + self.config.read(self.path) + + def write(self): + """Write the configuration to :attr:`path`""" + with open(self.path, 'w') as f: + self.config.write(f) + + @property + def languages(self): + return {Language.fromietf(l) for l in json.loads(self.config.get('general', 'languages'))} + + @languages.setter + def languages(self, value): + self.config.set('general', 'languages', json.dumps(sorted([str(l) for l in value]))) + + @property + def providers(self): + return json.loads(self.config.get('general', 'providers')) + + @providers.setter + def providers(self, value): + self.config.set('general', 'providers', json.dumps(sorted([p.lower() for p in value]))) + + @property + def single(self): + return self.config.getboolean('general', 'single') + + @single.setter + def single(self, value): + self.config.set('general', 'single', str(int(value))) + + @property + def embedded_subtitles(self): + return self.config.getboolean('general', 'embedded_subtitles') + + @embedded_subtitles.setter + def embedded_subtitles(self, value): + self.config.set('general', 'embedded_subtitles', str(int(value))) + + @property + def age(self): + return timedelta(seconds=self.config.getint('general', 'age')) + + @age.setter + def age(self, value): + self.config.set('general', 'age', str(int(value.total_seconds()))) + + @property + def hearing_impaired(self): + return self.config.getboolean('general', 'hearing_impaired') + + @hearing_impaired.setter + def hearing_impaired(self, value): + self.config.set('general', 'hearing_impaired', str(int(value))) + + @property + def min_score(self): + return self.config.getfloat('general', 'min_score') + + @min_score.setter + def min_score(self, value): + self.config.set('general', 'min_score', str(value)) + + @property + def provider_configs(self): + rv = {} + for provider in provider_manager: + if self.config.has_section(provider.name): + rv[provider.name] = {k: v for k, v in self.config.items(provider.name)} + return rv + + @provider_configs.setter + def provider_configs(self, value): + # loop over provider configurations + for provider, config in value.items(): + # create the corresponding section if necessary + if not self.config.has_section(provider): + self.config.add_section(provider) + + # add config options + for k, v in config.items(): + self.config.set(provider, k, v) + + class LanguageParamType(click.ParamType): """:class:`~click.ParamType` for languages that returns a :class:`~babelfish.language.Language`""" name = 'language' @@ -82,26 +195,33 @@ AGE = AgeParamType() PROVIDER = click.Choice(sorted(provider_manager.names())) -subliminal_cache = 'subliminal.dbm' +app_dir = click.get_app_dir('subliminal') +cache_file = 'subliminal.dbm' +config_file = 'config.ini' @click.group(context_settings={'max_content_width': 100}, epilog='Suggestions and bug reports are greatly appreciated: ' 'https://github.com/Diaoul/subliminal/') @click.option('--addic7ed', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD', help='Addic7ed configuration.') -@click.option('--cache-dir', type=click.Path(writable=True, resolve_path=True, file_okay=False), - default=click.get_app_dir('subliminal'), show_default=True, expose_value=True, - help='Path to the cache directory.') +@click.option('--opensubtitles', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD', + help='OpenSubtitles configuration.') +@click.option('--cache-dir', type=click.Path(writable=True, resolve_path=True, file_okay=False), default=app_dir, + show_default=True, expose_value=True, help='Path to the cache directory.') @click.option('--debug', is_flag=True, help='Print useful information for debugging subliminal and for reporting bugs.') @click.version_option(__version__) @click.pass_context -def subliminal(ctx, addic7ed, cache_dir, debug): +def subliminal(ctx, addic7ed, opensubtitles, cache_dir, debug): """Subtitles, faster than your thoughts.""" # create cache directory - os.makedirs(cache_dir, exist_ok=True) + try: + os.makedirs(cache_dir) + except OSError: + if not os.path.isdir(cache_dir): + raise # configure cache region.configure('dogpile.cache.dbm', expiration_time=timedelta(days=30), - arguments={'filename': os.path.join(cache_dir, subliminal_cache), 'lock_factory': MutexLock}) + arguments={'filename': os.path.join(cache_dir, cache_file), 'lock_factory': MutexLock}) # configure logging if debug: @@ -114,6 +234,8 @@ def subliminal(ctx, addic7ed, cache_dir, debug): ctx.obj = {'provider_configs': {}} if addic7ed: ctx.obj['provider_configs']['addic7ed'] = {'username': addic7ed[0], 'password': addic7ed[1]} + if opensubtitles: + ctx.obj['provider_configs']['opensubtitles'] = {'username': opensubtitles[0], 'password': opensubtitles[1]} @subliminal.command() @@ -123,7 +245,7 @@ def subliminal(ctx, addic7ed, cache_dir, debug): def cache(ctx, clear_subliminal): """Cache management.""" if clear_subliminal: - os.remove(os.path.join(ctx.parent.params['cache_dir'], subliminal_cache)) + os.remove(os.path.join(ctx.parent.params['cache_dir'], cache_file)) click.echo('Subliminal\'s cache cleared.') else: click.echo('Nothing done.') @@ -139,11 +261,11 @@ def cache(ctx, clear_subliminal): @click.option('-e', '--encoding', type=click.STRING, metavar='ENC', help='Subtitle file encoding, default is to ' 'preserve original encoding.') @click.option('-s', '--single', is_flag=True, default=False, help='Save subtitle without language code in the file ' - 'name, i.e. use .srt extension.') + 'name, i.e. use .srt extension. Do not use this unless your media player requires it.') @click.option('-f', '--force', is_flag=True, default=False, help='Force download even if a subtitle already exist.') @click.option('-hi', '--hearing-impaired', is_flag=True, default=False, help='Prefer hearing impaired subtitles.') @click.option('-m', '--min-score', type=click.IntRange(0, 100), default=0, help='Minimum score for a subtitle ' - 'to be downloaded.') + 'to be downloaded (0 to 100).') @click.option('-v', '--verbose', count=True, help='Increase verbosity.') @click.argument('path', type=click.Path(), required=True, nargs=-1) @click.pass_obj @@ -163,17 +285,31 @@ def download(obj, provider, language, age, directory, encoding, single, force, h # scan videos videos = [] ignored_videos = [] - with click.progressbar(path, label='Collecting videos', - item_show_func=lambda p: str(p) if p is not None else '') as bar: + errored_paths = [] + with click.progressbar(path, label='Collecting videos', item_show_func=lambda p: p or '') as bar: for p in bar: + logger.debug('Collecting path %s', p) + # non-existing if not os.path.exists(p): - videos.append(Video.fromname(p)) + try: + video = Video.fromname(p) + except: + logger.exception('Unexpected error while collecting non-existing path %s', p) + errored_paths.append(p) + continue + videos.append(video) continue # directories if os.path.isdir(p): - for video in scan_videos(p, subtitles=not force, embedded_subtitles=not force): + try: + scanned_videos = scan_videos(p, subtitles=not force, embedded_subtitles=not force) + except: + logger.exception('Unexpected error while collecting directory path %s', p) + errored_paths.append(p) + continue + for video in scanned_videos: if check_video(video, languages=language, age=age, undefined=single): videos.append(video) else: @@ -181,16 +317,26 @@ def download(obj, provider, language, age, directory, encoding, single, force, h continue # other inputs - video = scan_video(p, subtitles=not force, embedded_subtitles=not force) + try: + video = scan_video(p, subtitles=not force, embedded_subtitles=not force) + except: + logger.exception('Unexpected error while collecting path %s', p) + errored_paths.append(p) + continue if check_video(video, languages=language, age=age, undefined=single): videos.append(video) else: ignored_videos.append(video) + # output errored paths + if verbose > 0: + for p in errored_paths: + click.secho('%s errored' % p, fg='red') + # output ignored videos if verbose > 1: for video in ignored_videos: - click.secho('%s ignored - subtitles: %s / age: %d day%s ' % ( + click.secho('%s ignored - subtitles: %s / age: %d day%s' % ( os.path.split(video.name)[1], ', '.join(str(s) for s in video.subtitle_languages) or 'none', video.age.days, @@ -198,10 +344,14 @@ def download(obj, provider, language, age, directory, encoding, single, force, h ), fg='yellow') # report collected videos - click.echo('%s video%s collected / %s video%s ignored' % (click.style(str(len(videos)), bold=True), - 's' if len(videos) > 1 else '', - click.style(str(len(ignored_videos)), bold=True), - 's' if len(ignored_videos) > 1 else '')) + click.echo('%s video%s collected / %s video%s ignored / %s error%s' % ( + click.style(str(len(videos)), bold=True, fg='green' if videos else None), + 's' if len(videos) > 1 else '', + click.style(str(len(ignored_videos)), bold=True, fg='yellow' if ignored_videos else None), + 's' if len(ignored_videos) > 1 else '', + click.style(str(len(errored_paths)), bold=True, fg='red' if errored_paths else None), + 's' if len(errored_paths) > 1 else '', + )) # exit if no video collected if not videos: @@ -259,7 +409,7 @@ def download(obj, provider, language, age, directory, encoding, single, force, h scaled_score *= 100 / v.scores['hash'] # echo some nice colored output - click.echo(' - [{score}] - {language} subtitle from {provider_name} (match on {matches})'.format( + click.echo(' - [{score}] {language} subtitle from {provider_name} (match on {matches})'.format( score=click.style('{:5.1f}'.format(scaled_score), fg=score_color, bold=score >= v.scores['hash']), language=s.language.name if s.language.country is None else '%s (%s)' % (s.language.name, s.language.country.name), diff --git a/lib/subliminal/converters/addic7ed.py b/lib/subliminal/converters/addic7ed.py index 571f8ee11f41427a5c4bc863e19fa9c2768e6beb..f9cb83161506ef5737f50b6cae8089a62a6ce4e8 100644 --- a/lib/subliminal/converters/addic7ed.py +++ b/lib/subliminal/converters/addic7ed.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals from babelfish import LanguageReverseConverter, language_converters class Addic7edConverter(LanguageReverseConverter): def __init__(self): self.name_converter = language_converters['name'] - self.from_addic7ed = {'CatalĂ ': ('cat',), 'Chinese (Simplified)': ('zho',), 'Chinese (Traditional)': ('zho',), + self.from_addic7ed = {u'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',), diff --git a/lib/subliminal/converters/tvsubtitles.py b/lib/subliminal/converters/tvsubtitles.py index 9507df268f7e6d8470df51a3150c9499786e6b44..45b9fed1ae6e4d6abf2f22b7a570ffc2c899aa0e 100644 --- a/lib/subliminal/converters/tvsubtitles.py +++ b/lib/subliminal/converters/tvsubtitles.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals from babelfish import LanguageReverseConverter, language_converters diff --git a/lib/subliminal/exceptions.py b/lib/subliminal/exceptions.py index e1ac1c67b04318141f5a4ef9ed840c81f4fb2fe2..de2fabc0e1f10e37f80ec299b32cf0fc63849dfd 100644 --- a/lib/subliminal/exceptions.py +++ b/lib/subliminal/exceptions.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - - class Error(Exception): """Base class for exceptions in subliminal.""" pass diff --git a/lib/subliminal/providers/__init__.py b/lib/subliminal/providers/__init__.py index 4181c8b0bf1cb76d6a7f848eff569c53408682bd..f5ab8a306e7ea7b26b8528ef449d400d323a23c2 100644 --- a/lib/subliminal/providers/__init__.py +++ b/lib/subliminal/providers/__init__.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals import logging from bs4 import BeautifulSoup, FeatureNotFound diff --git a/lib/subliminal/providers/addic7ed.py b/lib/subliminal/providers/addic7ed.py index b0a86c2bfe91070b767fc0626cc097e4c8fbe0cc..3edfe357a00db97c81f4658e31fa39339264cad3 100644 --- a/lib/subliminal/providers/addic7ed.py +++ b/lib/subliminal/providers/addic7ed.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals import logging import re diff --git a/lib/subliminal/providers/napiprojekt.py b/lib/subliminal/providers/napiprojekt.py new file mode 100644 index 0000000000000000000000000000000000000000..c89db7fb6c75e333b609d14b6251b5d06561ec39 --- /dev/null +++ b/lib/subliminal/providers/napiprojekt.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +import logging + +from babelfish import Language +from requests import Session + +from . import Provider +from ..subtitle import Subtitle + +logger = logging.getLogger(__name__) + + +def get_subhash(hash): + """Get a second hash based on napiprojekt's hash. + + :param str hash: napiprojekt's hash. + :return: the subhash. + :rtype: str + + """ + idx = [0xe, 0x3, 0x6, 0x8, 0x2] + mul = [2, 2, 5, 4, 3] + add = [0, 0xd, 0x10, 0xb, 0x5] + + b = [] + for i in range(len(idx)): + a = add[i] + m = mul[i] + i = idx[i] + t = a + int(hash[i], 16) + v = int(hash[t:t + 2], 16) + b.append(('%x' % (v * m))[-1]) + + return ''.join(b) + + +class NapiProjektSubtitle(Subtitle): + provider_name = 'napiprojekt' + + def __init__(self, language, hash): + super(NapiProjektSubtitle, self).__init__(language) + self.hash = hash + + @property + def id(self): + return self.hash + + def get_matches(self, video, hearing_impaired=False): + matches = super(NapiProjektSubtitle, self).get_matches(video, hearing_impaired=hearing_impaired) + + # hash + if 'napiprojekt' in video.hashes and video.hashes['napiprojekt'] == self.hash: + matches.add('hash') + + return matches + + +class NapiProjektProvider(Provider): + languages = {Language.fromalpha2(l) for l in ['pl']} + required_hash = 'napiprojekt' + server_url = 'http://napiprojekt.pl/unit_napisy/dl.php' + + def initialize(self): + self.session = Session() + + def terminate(self): + self.session.close() + + def query(self, language, hash): + params = { + 'v': 'dreambox', + 'kolejka': 'false', + 'nick': '', + 'pass': '', + 'napios': 'Linux', + 'l': language.alpha2.upper(), + 'f': hash, + 't': get_subhash(hash)} + logger.info('Searching subtitle %r', params) + response = self.session.get(self.server_url, params=params, timeout=10) + response.raise_for_status() + + # handle subtitles not found and errors + if response.content[:4] == b'NPc0': + logger.debug('No subtitles found') + return None + + subtitle = NapiProjektSubtitle(language, hash) + subtitle.content = response.content + logger.debug('Found subtitle %r', subtitle) + + return subtitle + + def list_subtitles(self, video, languages): + return [s for s in [self.query(l, video.hashes['napiprojekt']) for l in languages] if s is not None] + + def download_subtitle(self, subtitle): + # there is no download step, content is already filled from listing subtitles + pass diff --git a/lib/subliminal/providers/opensubtitles.py b/lib/subliminal/providers/opensubtitles.py index 64962b04545403970de3c57ce3feb5bff995943c..9698c11de12aa9ccd276fcbb807f510a29069190 100644 --- a/lib/subliminal/providers/opensubtitles.py +++ b/lib/subliminal/providers/opensubtitles.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals import base64 import logging import os @@ -12,7 +11,7 @@ from six.moves.xmlrpc_client import ServerProxy from . import Provider, TimeoutSafeTransport, get_version from .. import __version__ -from ..exceptions import AuthenticationError, DownloadLimitExceeded, ProviderError +from ..exceptions import AuthenticationError, ConfigurationError, DownloadLimitExceeded, ProviderError from ..subtitle import Subtitle, fix_line_ending, guess_matches from ..video import Episode, Movie @@ -95,13 +94,19 @@ class OpenSubtitlesSubtitle(Subtitle): class OpenSubtitlesProvider(Provider): languages = {Language.fromopensubtitles(l) for l in language_converters['opensubtitles'].codes} - def __init__(self): + def __init__(self, username=None, password=None): self.server = ServerProxy('https://api.opensubtitles.org/xml-rpc', TimeoutSafeTransport(10)) + if username and not password or not username and password: + raise ConfigurationError('Username and password must be specified') + # None values not allowed for logging in, so replace it by '' + self.username = username or '' + self.password = password or '' self.token = None def initialize(self): logger.info('Logging in') - response = checked(self.server.LogIn('', '', 'eng', 'subliminal v%s' % get_version(__version__))) + response = checked(self.server.LogIn(self.username, self.password, 'eng', + 'subliminal v%s' % get_version(__version__))) self.token = response['token'] logger.debug('Logged in with token %r', self.token) @@ -109,6 +114,7 @@ class OpenSubtitlesProvider(Provider): logger.info('Logging out') checked(self.server.LogOut(self.token)) self.server.close() + self.token = None logger.debug('Logged out') def no_operation(self): @@ -140,7 +146,7 @@ class OpenSubtitlesProvider(Provider): # exit if no data if not response['data']: - logger.info('No subtitles found') + logger.debug('No subtitles found') return subtitles # loop over subtitle items diff --git a/lib/subliminal/providers/podnapisi.py b/lib/subliminal/providers/podnapisi.py index c83145a44a4132a91d4d48b6c38b5dedf46c322c..2790c6fa0ea60fb7043bbc1dd80626098aecec88 100644 --- a/lib/subliminal/providers/podnapisi.py +++ b/lib/subliminal/providers/podnapisi.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals import io import logging import re @@ -121,7 +120,9 @@ class PodnapisiProvider(Provider): releases = [] if subtitle_xml.find('release').text: for release in subtitle_xml.find('release').text.split(): - releases.append(re.sub(r'\.+$', '', release)) # remove trailing dots + release = re.sub(r'\.+$', '', release) # remove trailing dots + release = ''.join(filter(lambda x: ord(x) < 128, release)) # remove non-ascii characters + releases.append(release) title = subtitle_xml.find('title').text season = int(subtitle_xml.find('tvSeason').text) episode = int(subtitle_xml.find('tvEpisode').text) diff --git a/lib/subliminal/providers/thesubdb.py b/lib/subliminal/providers/thesubdb.py index 0cc890e9425599c2cede0bf0fb7e4517a6d76486..bb82f8af55d567618c8948e041c34637b2ad2ef0 100644 --- a/lib/subliminal/providers/thesubdb.py +++ b/lib/subliminal/providers/thesubdb.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals import logging from babelfish import Language @@ -65,7 +64,7 @@ class TheSubDBProvider(Provider): language = Language.fromalpha2(language_code) subtitle = TheSubDBSubtitle(language, hash) - logger.info('Found subtitle %r', subtitle) + logger.debug('Found subtitle %r', subtitle) subtitles.append(subtitle) return subtitles diff --git a/lib/subliminal/providers/tvsubtitles.py b/lib/subliminal/providers/tvsubtitles.py index 5e60bc9e4af04b085af49d288416c3b2c8ffceca..06fd878a933dc83650aa4e04e423cbac6b03a582 100644 --- a/lib/subliminal/providers/tvsubtitles.py +++ b/lib/subliminal/providers/tvsubtitles.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals import io import logging import re @@ -181,7 +180,7 @@ class TVsubtitlesProvider(Provider): subtitle = TVsubtitlesSubtitle(language, page_link, subtitle_id, series, season, episode, year, rip, release) - logger.info('Found subtitle %s', subtitle) + logger.debug('Found subtitle %s', subtitle) subtitles.append(subtitle) return subtitles diff --git a/lib/subliminal/score.py b/lib/subliminal/score.py index 831314bc28b8927c39c07fcdc4e294329fa3e89a..fe417b44327c146cb52a567ff93229c75bd49eab 100755 --- a/lib/subliminal/score.py +++ b/lib/subliminal/score.py @@ -32,7 +32,7 @@ The :meth:`Subtitle.get_matches <subliminal.subtitle.Subtitle.get_matches>` meth :func:`~subliminal.subtitle.compute_score` computes the score. """ -from __future__ import unicode_literals, print_function +from __future__ import print_function from sympy import Eq, solve, symbols diff --git a/lib/subliminal/subtitle.py b/lib/subliminal/subtitle.py index 68f5984249df41a8d5c59a56390dd82890c3ee8d..eeb840a8023fe713a9d29a27d1b5dcb87edc6bf1 100644 --- a/lib/subliminal/subtitle.py +++ b/lib/subliminal/subtitle.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals import logging import os diff --git a/lib/subliminal/video.py b/lib/subliminal/video.py index 7e52502e3b4758a5dce3e93a08ddc6320f49d128..cc916ed1b086a12d4c13d65c4d05478a752f274c 100644 --- a/lib/subliminal/video.py +++ b/lib/subliminal/video.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals, division +from __future__ import division from datetime import datetime, timedelta import hashlib import logging @@ -110,6 +110,11 @@ class Video(object): @classmethod def fromname(cls, name): + """Shortcut for :meth:`fromguess` with a `guess` guessed from the `name`. + + :param str name: name of the video. + + """ return cls.fromguess(name, guess_file_info(name)) def __repr__(self): @@ -243,11 +248,6 @@ def search_external_subtitles(path): fileroot, fileext = os.path.splitext(filename) subtitles = {} for p in os.listdir(dirpath): - # skip badly encoded filenames - if isinstance(p, bytes): # pragma: no cover - logger.error('Skipping badly encoded filename %r in %r', p.decode('utf-8', errors='replace'), dirpath) - continue - # keep only valid subtitle filenames if not p.startswith(fileroot) or not p.endswith(SUBTITLE_EXTENSIONS): continue @@ -302,6 +302,7 @@ def scan_video(path, subtitles=True, embedded_subtitles=True): logger.debug('Size is %d', video.size) video.hashes['opensubtitles'] = hash_opensubtitles(path) video.hashes['thesubdb'] = hash_thesubdb(path) + video.hashes['napiprojekt'] = hash_napiprojekt(path) logger.debug('Computed hashes %r', video.hashes) else: logger.warning('Size is lower than 10MB: hashes not computed') @@ -390,8 +391,7 @@ def scan_video(path, subtitles=True, embedded_subtitles=True): def scan_videos(path, subtitles=True, embedded_subtitles=True): """Scan `path` for videos and their subtitles. - :params path: existing directory path to scan. - :type path: str + :param str path: existing directory path to scan. :param bool subtitles: scan for subtitles with the same name. :param bool embedded_subtitles: scan for embedded subtitles. :return: the scanned videos. @@ -409,31 +409,16 @@ def scan_videos(path, subtitles=True, embedded_subtitles=True): # walk the path videos = [] for dirpath, dirnames, filenames in os.walk(path): - # skip badly encoded directory names - if isinstance(dirpath, bytes): # pragma: no cover - logger.error('Skipping badly encoded directory %r', dirpath.decode('utf-8', errors='replace')) - continue - logger.debug('Walking directory %s', dirpath) # remove badly encoded and hidden dirnames for dirname in list(dirnames): - if isinstance(dirname, bytes): # pragma: no cover - logger.error('Skipping badly encoded dirname %r in %r', dirname.decode('utf-8', errors='replace'), - dirpath) - dirnames.remove(dirname) - elif dirname.startswith('.'): + if dirname.startswith('.'): logger.debug('Skipping hidden dirname %r in %r', dirname, dirpath) dirnames.remove(dirname) # scan for videos for filename in filenames: - # skip badly encoded filenames - if isinstance(filename, bytes): # pragma: no cover - logger.error('Skipping badly encoded filename %r in %r', filename.decode('utf-8', errors='replace'), - dirpath) - continue - # filter on videos if not filename.endswith(VIDEO_EXTENSIONS): continue @@ -510,3 +495,17 @@ def hash_thesubdb(video_path): data += f.read(readsize) return hashlib.md5(data).hexdigest() + + +def hash_napiprojekt(video_path): + """Compute a hash using NapiProjekt's algorithm. + + :param str video_path: path of the video. + :return: the hash. + :rtype: str + + """ + readsize = 1024 * 1024 * 10 + with open(video_path, 'rb') as f: + data = f.read(readsize) + return hashlib.md5(data).hexdigest() diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index ca3f7d8dcb1df1dbebb77283cf030162178a34c3..251a47393928fe6df97cf61a7bec2ac8547f3b7a 100644 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -1232,6 +1232,11 @@ def initialize(consoleLogging=True): if hasattr(curTorrentProvider, 'ranked'): curTorrentProvider.ranked = bool(check_setting_int(CFG, curTorrentProvider.getID().upper(), curTorrentProvider.getID() + '_ranked', 0)) + + if hasattr(curTorrentProvider, 'engrelease'): + curTorrentProvider.engrelease = bool(check_setting_int(CFG, curTorrentProvider.getID().upper(), + curTorrentProvider.getID() + '_engrelease', 0)) + if hasattr(curTorrentProvider, 'sorting'): curTorrentProvider.sorting = check_setting_str(CFG, curTorrentProvider.getID().upper(), curTorrentProvider.getID() + '_sorting','seeders') @@ -1785,6 +1790,9 @@ def save_config(): if hasattr(curTorrentProvider, 'ranked'): new_config[curTorrentProvider.getID().upper()][curTorrentProvider.getID() + '_ranked'] = int( curTorrentProvider.ranked) + if hasattr(curTorrentProvider, 'engrelease'): + new_config[curTorrentProvider.getID().upper()][curTorrentProvider.getID() + '_engrelease'] = int( + curTorrentProvider.engrelease) if hasattr(curTorrentProvider, 'sorting'): new_config[curTorrentProvider.getID().upper()][curTorrentProvider.getID() + '_sorting'] = curTorrentProvider.sorting if hasattr(curTorrentProvider, 'ratio'): diff --git a/sickbeard/postProcessor.py b/sickbeard/postProcessor.py index df6d9db731cfff5f326b78f0de2258be85e57df4..fd82df824a5bbdddbe785c05d45423829b43b749 100644 --- a/sickbeard/postProcessor.py +++ b/sickbeard/postProcessor.py @@ -853,6 +853,10 @@ class PostProcessor(object): self._log(u"File " + self.file_path + " seems to be a directory") return False + if not ek(os.path.exists, self.file_path): + self._log(u"File " + self.file_path + " doesn't exist, did unrar fail?") + return False + for ignore_file in self.IGNORED_FILESTRINGS: if ignore_file in self.file_path: self._log(u"File " + self.file_path + " is ignored type, skipping") diff --git a/sickbeard/providers/tntvillage.py b/sickbeard/providers/tntvillage.py index cbe223c257e8ebf578a659e34c4060d4904c24bd..98ca49f663d690a34cb05147b0ad0d54eea57f1e 100644 --- a/sickbeard/providers/tntvillage.py +++ b/sickbeard/providers/tntvillage.py @@ -77,6 +77,7 @@ class TNTVillageProvider(generic.TorrentProvider): self.password = None self.ratio = None self.cat = None + self.engrelease = None self.page = 10 self.subtitle = None self.minseed = None @@ -244,15 +245,28 @@ class TNTVillageProvider(generic.TorrentProvider): continue if re.search("ita", name.split(sub)[0], re.I): - logger.log(u"Found Italian release", logger.DEBUG) + logger.log(u"Found Italian release: " + name, logger.DEBUG) italian = True break if not subFound and re.search("ita", name, re.I): - logger.log(u"Found Italian release", logger.DEBUG) + logger.log(u"Found Italian release: " + name, logger.DEBUG) italian = True return italian + + def _is_english(self, torrent_rows): + + name = str(torrent_rows.find_all('td')[1].find('b').find('span')) + if not name or name is 'None': + return False + + english = False + if re.search("eng", name, re.I): + logger.log(u"Found English release: " + name, logger.DEBUG) + english = True + + return english def _is_season_pack(self, name): @@ -369,6 +383,10 @@ class TNTVillageProvider(generic.TorrentProvider): logger.log(u"Torrent is subtitled, skipping: %s " % title, logger.DEBUG) continue + if self.engrelease and not self._is_english(result): + logger.log(u"Torrent isnt english audio/subtitled , skipping: %s " % title, logger.DEBUG) + continue + search_show = re.split(r'([Ss][\d{1,2}]+)', search_string)[0] show_title = search_show rindex = re.search(r'([Ss][\d{1,2}]+)', title) diff --git a/sickbeard/subtitles.py b/sickbeard/subtitles.py index 25747df81af83cb8e13c2e05c2d9dff06ed81c14..00d697bf13bd55902eaeaf2cfaec5c532d7a1e77 100644 --- a/sickbeard/subtitles.py +++ b/sickbeard/subtitles.py @@ -39,6 +39,7 @@ distribution = pkg_resources.Distribution(location=os.path.dirname(os.path.dirna entry_points = { 'subliminal.providers': [ 'addic7ed = subliminal.providers.addic7ed:Addic7edProvider', + 'napiprojekt = subliminal.providers.napiprojekt:NapiProjektProvider', 'opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider', 'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider', 'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider', @@ -58,6 +59,7 @@ subliminal.region.configure('dogpile.cache.memory') provider_urls = { 'addic7ed': 'http://www.addic7ed.com', + 'napiprojekt': 'http://www.napiprojekt.pl', 'opensubtitles': 'http://www.opensubtitles.org', 'podnapisi': 'http://www.podnapisi.net', 'thesubdb': 'http://www.thesubdb.com', @@ -123,7 +125,7 @@ def downloadSubtitles(subtitles_info): video = subliminal.scan_video(video_path, subtitles=False, embedded_subtitles=False) except Exception: logger.log(u'%s: Exception caught in subliminal.scan_video for S%02dE%02d' % - (subtitles_info['indexerid'], subtitles_info['season'], subtitles_info['episode']), logger.DEBUG) + (subtitles_info['show.indexerid'], subtitles_info['season'], subtitles_info['episode']), logger.DEBUG) return try: diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 52449e566097cd2cb17ef26547f7dbfa64250efa..bce3acd08b7435d0af33b4a810e12094e6d8c42b 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -1473,21 +1473,22 @@ class Home(WebRoot): return self.redirect("/home/displayShow?show=%i" % show.indexerid) def deleteShow(self, show=None, full=0): - error, show = Show.delete(show, full) - - if error is not None: - return self._genericMessage('Error', error) - - ui.notifications.message( - '%s has been %s %s' % - ( - show.name, - ('deleted', 'trashed')[bool(sickbeard.TRASH_REMOVE_SHOW)], - ('(media untouched)', '(with all related media)')[bool(full)] + if show: + error, show = Show.delete(show, full) + + if error is not None: + return self._genericMessage('Error', error) + + ui.notifications.message( + '%s has been %s %s' % + ( + show.name, + ('deleted', 'trashed')[bool(sickbeard.TRASH_REMOVE_SHOW)], + ('(media untouched)', '(with all related media)')[bool(full)] + ) ) - ) - - time.sleep(cpu_presets[sickbeard.CPU_PRESET]) + + time.sleep(cpu_presets[sickbeard.CPU_PRESET]) # Don't redirect to the default page, so the user can confirm that the show was deleted return self.redirect('/home/') @@ -4418,7 +4419,14 @@ class ConfigProviders(Config): kwargs[curTorrentProvider.getID() + '_ranked']) except: curTorrentProvider.ranked = 0 - + + if hasattr(curTorrentProvider, 'engrelease'): + try: + curTorrentProvider.engrelease = config.checkbox_to_value( + kwargs[curTorrentProvider.getID() + '_engrelease']) + except: + curTorrentProvider.engrelease = 0 + if hasattr(curTorrentProvider, 'sorting'): try: curTorrentProvider.sorting = str(kwargs[curTorrentProvider.getID() + '_sorting']).strip() diff --git a/tests/search_tests.py b/tests/search_tests.py index 5f00ccce8de8351b6aa517a3fbde3858b46ea46e..c90378db4430547c0011d79c593374f3d20c85f0 100755 --- a/tests/search_tests.py +++ b/tests/search_tests.py @@ -103,8 +103,8 @@ def test_generator(curData, name, provider, forceSearch): title, url = provider._get_title_and_url(items[0]) for word in show.name.split(" "): - if not word in title: - print "Show name not in title: %s" % title + if not word.lower() in title.lower(): + print "Show name not in title: %s. URL: %s" % (title, url) continue if not url: