diff --git a/subliminal/cli.py b/subliminal/cli.py index cc24853c2..7091fdebc 100644 --- a/subliminal/cli.py +++ b/subliminal/cli.py @@ -65,6 +65,7 @@ def __init__(self, path): self.config.set('general', 'providers', json.dumps(sorted([p.name for p in provider_manager]))) self.config.set('general', 'refiners', json.dumps(sorted([r.name for r in refiner_manager]))) self.config.set('general', 'single', str(0)) + self.config.set('general', 'foreign_only', 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)) @@ -135,6 +136,14 @@ def hearing_impaired(self): def hearing_impaired(self, value): self.config.set('general', 'hearing_impaired', str(int(value))) + @property + def foreign_only(self): + return self.config.getboolean('general', 'foreign_only') + + @foreign_only.setter + def foreign_only(self, value): + self.config.set('general', 'foreign_only', str(int(value))) + @property def min_score(self): return self.config.getfloat('general', 'min_score') @@ -285,6 +294,7 @@ def cache(ctx, clear_subliminal): '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('-fo', '--foreign-only', is_flag=True, default=False, help='Prefer foreign parts only subtitles.') @click.option('-m', '--min-score', type=click.IntRange(0, 100), default=0, help='Minimum score for a subtitle ' 'to be downloaded (0 to 100).') @click.option('-w', '--max-workers', type=click.IntRange(1, 50), default=None, help='Maximum number of threads to use.') @@ -293,7 +303,7 @@ def cache(ctx, clear_subliminal): @click.option('-v', '--verbose', count=True, help='Increase verbosity.') @click.argument('path', type=click.Path(), required=True, nargs=-1) @click.pass_obj -def download(obj, provider, refiner, language, age, directory, encoding, single, force, hearing_impaired, min_score, +def download(obj, provider, refiner, language, age, directory, encoding, single, force, hearing_impaired, foreign_only, min_score, max_workers, archives, verbose, path): """Download best subtitles. @@ -400,7 +410,7 @@ def download(obj, provider, refiner, language, age, directory, encoding, single, scores = get_scores(v) subtitles = p.download_best_subtitles(p.list_subtitles(v, language - v.subtitle_languages), v, language, min_score=scores['hash'] * min_score / 100, - hearing_impaired=hearing_impaired, only_one=single) + hearing_impaired=hearing_impaired, foreign_only=foreign_only, only_one=single) downloaded_subtitles[v] = subtitles if p.discarded_providers: @@ -445,6 +455,8 @@ def download(obj, provider, refiner, language, age, directory, encoding, single, scaled_score = score if s.hearing_impaired == hearing_impaired: scaled_score -= scores['hearing_impaired'] + if s.foreign_only == foreign_only: + scaled_score -= scores['foreign_only'] scaled_score *= 100 / scores['hash'] # echo some nice colored output diff --git a/subliminal/core.py b/subliminal/core.py index c516c49d3..0dfe5180d 100644 --- a/subliminal/core.py +++ b/subliminal/core.py @@ -185,7 +185,7 @@ def download_subtitle(self, subtitle): return True - def download_best_subtitles(self, subtitles, video, languages, min_score=0, hearing_impaired=False, only_one=False, + def download_best_subtitles(self, subtitles, video, languages, min_score=0, hearing_impaired=False, foreign_only=False, only_one=False, compute_score=None): """Download the best matching subtitles. @@ -197,9 +197,10 @@ def download_best_subtitles(self, subtitles, video, languages, min_score=0, hear :type languages: set of :class:`~babelfish.language.Language` :param int min_score: minimum score for a subtitle to be downloaded. :param bool hearing_impaired: hearing impaired preference. + :param bool foreign_only: foreign parts only preference. :param bool only_one: download only one subtitle, not one per language. :param compute_score: function that takes `subtitle` and `video` as positional arguments, - `hearing_impaired` as keyword argument and returns the score. + `hearing_impaired` as keyword argument, `foreign_only` as keyword argument and returns the score. :return: downloaded subtitles. :rtype: list of :class:`~subliminal.subtitle.Subtitle` @@ -207,7 +208,7 @@ def download_best_subtitles(self, subtitles, video, languages, min_score=0, hear compute_score = compute_score or default_compute_score # sort subtitles by score - scored_subtitles = sorted([(s, compute_score(s, video, hearing_impaired=hearing_impaired)) + scored_subtitles = sorted([(s, compute_score(s, video, hearing_impaired=hearing_impaired, foreign_only=foreign_only)) for s in subtitles], key=operator.itemgetter(1), reverse=True) # download best subtitles, falling back on the next on error @@ -601,7 +602,7 @@ def download_subtitles(subtitles, pool_class=ProviderPool, **kwargs): pool.download_subtitle(subtitle) -def download_best_subtitles(videos, languages, min_score=0, hearing_impaired=False, only_one=False, compute_score=None, +def download_best_subtitles(videos, languages, min_score=0, hearing_impaired=False, foreign_only=False, only_one=False, compute_score=None, pool_class=ProviderPool, **kwargs): """List and download the best matching subtitles. @@ -613,9 +614,10 @@ def download_best_subtitles(videos, languages, min_score=0, hearing_impaired=Fal :type languages: set of :class:`~babelfish.language.Language` :param int min_score: minimum score for a subtitle to be downloaded. :param bool hearing_impaired: hearing impaired preference. + :param bool foreign_only: foreign parts only preference. :param bool only_one: download only one subtitle, not one per language. :param compute_score: function that takes `subtitle` and `video` as positional arguments, - `hearing_impaired` as keyword argument and returns the score. + `hearing_impaired` as keyword argument, `foreign_only` as keyword argument and returns the score. :param pool_class: class to use as provider pool. :type pool_class: :class:`ProviderPool`, :class:`AsyncProviderPool` or similar :param \*\*kwargs: additional parameters for the provided `pool_class` constructor. @@ -643,7 +645,8 @@ def download_best_subtitles(videos, languages, min_score=0, hearing_impaired=Fal logger.info('Downloading best subtitles for %r', video) subtitles = pool.download_best_subtitles(pool.list_subtitles(video, languages - video.subtitle_languages), video, languages, min_score=min_score, - hearing_impaired=hearing_impaired, only_one=only_one, + hearing_impaired=hearing_impaired, + foreign_only=foreign_only, only_one=only_one, compute_score=compute_score) logger.info('Downloaded %d subtitle(s)', len(subtitles)) downloaded_subtitles[video].extend(subtitles) @@ -680,11 +683,11 @@ def save_subtitles(video, subtitles, single=False, directory=None, encoding=None # check language if subtitle.language in set(s.language for s in saved_subtitles): - logger.debug('Skipping subtitle %r: language already saved', subtitle) - continue + logger.debug('Skipping subtitle %r: language already saved', subtitle) + continue # create subtitle path - subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language) + subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language, subtitle.foreign_only) if directory is not None: subtitle_path = os.path.join(directory, os.path.split(subtitle_path)[1]) diff --git a/subliminal/providers/addic7ed.py b/subliminal/providers/addic7ed.py index 0d4a58fda..6fb46f510 100644 --- a/subliminal/providers/addic7ed.py +++ b/subliminal/providers/addic7ed.py @@ -27,9 +27,9 @@ class Addic7edSubtitle(Subtitle): """Addic7ed Subtitle.""" provider_name = 'addic7ed' - def __init__(self, language, hearing_impaired, page_link, series, season, episode, title, year, version, + def __init__(self, language, hearing_impaired, foreign_only, page_link, series, season, episode, title, year, version, download_link): - super(Addic7edSubtitle, self).__init__(language, hearing_impaired, page_link) + super(Addic7edSubtitle, self).__init__(language, hearing_impaired, foreign_only, page_link) self.series = series self.season = season self.episode = episode @@ -255,6 +255,7 @@ def query(self, series, season, year=None, country=None): # read the item language = Language.fromaddic7ed(cells[3].text) hearing_impaired = bool(cells[6].text) + foreign_only = False page_link = self.server_url + cells[2].a['href'][1:] season = int(cells[0].text) episode = int(cells[1].text) @@ -262,7 +263,7 @@ def query(self, series, season, year=None, country=None): version = cells[4].text download_link = cells[9].a['href'][1:] - subtitle = Addic7edSubtitle(language, hearing_impaired, page_link, series, season, episode, title, year, + subtitle = Addic7edSubtitle(language, hearing_impaired, foreign_only, page_link, series, season, episode, title, year, version, download_link) logger.debug('Found subtitle %r', subtitle) subtitles.append(subtitle) diff --git a/subliminal/providers/legendastv.py b/subliminal/providers/legendastv.py index cdd16aca2..94f01d5cf 100644 --- a/subliminal/providers/legendastv.py +++ b/subliminal/providers/legendastv.py @@ -112,7 +112,7 @@ def __init__(self, language, type, title, year, imdb_id, season, archive, name): def id(self): return '%s-%s' % (self.archive.id, self.name.lower()) - def get_matches(self, video, hearing_impaired=False): + def get_matches(self, video, hearing_impaired=False, foreign_only=False): matches = set() # episode diff --git a/subliminal/providers/opensubtitles.py b/subliminal/providers/opensubtitles.py index 5ab09da48..f715e7c13 100644 --- a/subliminal/providers/opensubtitles.py +++ b/subliminal/providers/opensubtitles.py @@ -24,9 +24,9 @@ class OpenSubtitlesSubtitle(Subtitle): provider_name = 'opensubtitles' series_re = re.compile(r'^"(?P.*)" (?P.*)$') - def __init__(self, language, hearing_impaired, page_link, subtitle_id, matched_by, movie_kind, hash, movie_name, + def __init__(self, language, hearing_impaired, foreign_only, page_link, subtitle_id, matched_by, movie_kind, hash, movie_name, movie_release_name, movie_year, movie_imdb_id, series_season, series_episode, filename, encoding): - super(OpenSubtitlesSubtitle, self).__init__(language, hearing_impaired, page_link, encoding) + super(OpenSubtitlesSubtitle, self).__init__(language, hearing_impaired, foreign_only, page_link, encoding) self.subtitle_id = subtitle_id self.matched_by = matched_by self.movie_kind = movie_kind @@ -185,6 +185,7 @@ def query(self, languages, hash=None, size=None, imdb_id=None, query=None, seaso # read the item language = Language.fromopensubtitles(subtitle_item['SubLanguageID']) hearing_impaired = bool(int(subtitle_item['SubHearingImpaired'])) + foreign_only = bool(int(subtitle_item['SubForeignPartsOnly'])) page_link = subtitle_item['SubtitlesLink'] subtitle_id = int(subtitle_item['IDSubtitleFile']) matched_by = subtitle_item['MatchedBy'] @@ -199,7 +200,7 @@ def query(self, languages, hash=None, size=None, imdb_id=None, query=None, seaso filename = subtitle_item['SubFileName'] encoding = subtitle_item.get('SubEncoding') or None - subtitle = OpenSubtitlesSubtitle(language, hearing_impaired, page_link, subtitle_id, matched_by, movie_kind, + subtitle = OpenSubtitlesSubtitle(language, hearing_impaired, foreign_only, page_link, subtitle_id, matched_by, movie_kind, hash, movie_name, movie_release_name, movie_year, movie_imdb_id, series_season, series_episode, filename, encoding) logger.debug('Found subtitle %r by %s', subtitle, matched_by) diff --git a/subliminal/providers/podnapisi.py b/subliminal/providers/podnapisi.py index f643682b9..447eea835 100644 --- a/subliminal/providers/podnapisi.py +++ b/subliminal/providers/podnapisi.py @@ -29,9 +29,9 @@ class PodnapisiSubtitle(Subtitle): """Podnapisi Subtitle.""" provider_name = 'podnapisi' - def __init__(self, language, hearing_impaired, page_link, pid, releases, title, season=None, episode=None, + def __init__(self, language, hearing_impaired, foreign_only, page_link, pid, releases, title, season=None, episode=None, year=None): - super(PodnapisiSubtitle, self).__init__(language, hearing_impaired, page_link) + super(PodnapisiSubtitle, self).__init__(language, hearing_impaired, foreign_only, page_link) self.pid = pid self.releases = releases self.title = title @@ -120,6 +120,7 @@ def query(self, language, keyword, season=None, episode=None, year=None): # read xml elements language = Language.fromietf(subtitle_xml.find('language').text) hearing_impaired = 'n' in (subtitle_xml.find('flags').text or '') + foreign_only = False page_link = subtitle_xml.find('url').text pid = subtitle_xml.find('pid').text releases = [] @@ -134,10 +135,10 @@ def query(self, language, keyword, season=None, episode=None, year=None): year = int(subtitle_xml.find('year').text) if is_episode: - subtitle = PodnapisiSubtitle(language, hearing_impaired, page_link, pid, releases, title, + subtitle = PodnapisiSubtitle(language, hearing_impaired, foreign_only, page_link, pid, releases, title, season=season, episode=episode, year=year) else: - subtitle = PodnapisiSubtitle(language, hearing_impaired, page_link, pid, releases, title, + subtitle = PodnapisiSubtitle(language, hearing_impaired, foreign_only, page_link, pid, releases, title, year=year) # ignore duplicates, see http://www.podnapisi.net/forum/viewtopic.php?f=62&t=26164&start=10#p213321 diff --git a/subliminal/providers/subscenter.py b/subliminal/providers/subscenter.py index 1e25e5e1d..6a133791e 100644 --- a/subliminal/providers/subscenter.py +++ b/subliminal/providers/subscenter.py @@ -25,9 +25,9 @@ class SubsCenterSubtitle(Subtitle): """SubsCenter Subtitle.""" provider_name = 'subscenter' - def __init__(self, language, hearing_impaired, page_link, series, season, episode, title, subtitle_id, subtitle_key, + def __init__(self, language, hearing_impaired, foreign_only, page_link, series, season, episode, title, subtitle_id, subtitle_key, downloaded, releases): - super(SubsCenterSubtitle, self).__init__(language, hearing_impaired, page_link) + super(SubsCenterSubtitle, self).__init__(language, hearing_impaired, foreign_only, page_link) self.series = series self.season = season self.episode = episode @@ -187,6 +187,7 @@ def query(self, title, season=None, episode=None): # read the item language = Language.fromalpha2(language_code) hearing_impaired = bool(subtitle_item['hearing_impaired']) + foreign_only = False subtitle_id = subtitle_item['id'] subtitle_key = subtitle_item['key'] downloaded = subtitle_item['downloaded'] @@ -200,7 +201,7 @@ def query(self, title, season=None, episode=None): continue # otherwise create it - subtitle = SubsCenterSubtitle(language, hearing_impaired, page_link, title, season, episode, + subtitle = SubsCenterSubtitle(language, hearing_impaired, foreign_only, page_link, title, season, episode, title, subtitle_id, subtitle_key, downloaded, [release]) logger.debug('Found subtitle %r', subtitle) subtitles[subtitle_id] = subtitle diff --git a/subliminal/score.py b/subliminal/score.py index 31ccb3433..8110a9f04 100755 --- a/subliminal/score.py +++ b/subliminal/score.py @@ -21,6 +21,7 @@ * audio_codec * resolution * hearing_impaired + * foreign_only * video_codec * series_imdb_id * imdb_id @@ -37,11 +38,11 @@ #: Scores for episodes episode_scores = {'hash': 359, 'series': 180, 'year': 90, 'season': 30, 'episode': 30, 'release_group': 15, - 'format': 7, 'audio_codec': 3, 'resolution': 2, 'video_codec': 2, 'hearing_impaired': 1} + 'format': 7, 'audio_codec': 3, 'resolution': 2, 'video_codec': 2, 'hearing_impaired': 1, 'foreign_only': 80} #: Scores for movies movie_scores = {'hash': 119, 'title': 60, 'year': 30, 'release_group': 15, - 'format': 7, 'audio_codec': 3, 'resolution': 2, 'video_codec': 2, 'hearing_impaired': 1} + 'format': 7, 'audio_codec': 3, 'resolution': 2, 'video_codec': 2, 'hearing_impaired': 1, 'foreign_only': 80} #: Equivalent release groups equivalent_release_groups = ({'LOL', 'DIMENSION'}, {'ASAP', 'IMMERSE', 'FLEET'}) @@ -81,8 +82,8 @@ def get_scores(video): raise ValueError('video must be an instance of Episode or Movie') -def compute_score(subtitle, video, hearing_impaired=None): - """Compute the score of the `subtitle` against the `video` with `hearing_impaired` preference. +def compute_score(subtitle, video, hearing_impaired=None, foreign_only=None): + """Compute the score of the `subtitle` against the `video` with `hearing_impaired` and/or `foreign_only` preference. :func:`compute_score` uses the :meth:`Subtitle.get_matches ` method and applies the scores (either from :data:`episode_scores` or :data:`movie_scores`) after some processing. @@ -92,11 +93,12 @@ def compute_score(subtitle, video, hearing_impaired=None): :param video: the video to compute the score against. :type video: :class:`~subliminal.video.Video` :param bool hearing_impaired: hearing impaired preference. + :param bool foreign_only: foreign parts only preference. :return: score of the subtitle. :rtype: int """ - logger.info('Computing score of %r for video %r with %r', subtitle, video, dict(hearing_impaired=hearing_impaired)) + logger.info('Computing score of %r for video %r with %r', subtitle, video, dict(hearing_impaired=hearing_impaired, foreign_only=foreign_only)) # get the scores dict scores = get_scores(video) @@ -138,12 +140,17 @@ def compute_score(subtitle, video, hearing_impaired=None): logger.debug('Matched hearing_impaired') matches.add('hearing_impaired') + # handle foreign only + if foreign_only is not None and subtitle.foreign_only == foreign_only: + logger.debug('Matched foreign_only') + matches.add('foreign_only') + # compute the score score = sum((scores.get(match, 0) for match in matches)) logger.info('Computed score %r with final matches %r', score, matches) # ensure score is within valid bounds - assert 0 <= score <= scores['hash'] + scores['hearing_impaired'] + assert 0 <= score <= scores['hash'] + scores['hearing_impaired'] + scores['foreign_only'] return score @@ -154,6 +161,7 @@ def solve_episode_equations(): hash, series, year, season, episode, release_group = symbols('hash series year season episode release_group') format, audio_codec, resolution, video_codec = symbols('format audio_codec resolution video_codec') hearing_impaired = symbols('hearing_impaired') + foreign_only = symbols('foreign_only') equations = [ # hash is best @@ -184,14 +192,17 @@ def solve_episode_equations(): Eq(resolution, video_codec), # video_codec is the least valuable match but counts more than the sum of all scoring increasing matches - Eq(video_codec, hearing_impaired + 1), + Eq(video_codec, (hearing_impaired + foreign_only) + 1), # hearing impaired is only used for score increasing, so put it to 1 Eq(hearing_impaired, 1), + + # foreign only is only used for score increasing, so put it to 1 + Eq(foreign_only, 1), ] return solve(equations, [hash, series, year, season, episode, release_group, format, audio_codec, resolution, - hearing_impaired, video_codec]) + hearing_impaired, foreign_only, video_codec]) def solve_movie_equations(): @@ -200,6 +211,7 @@ def solve_movie_equations(): hash, title, year, release_group = symbols('hash title year release_group') format, audio_codec, resolution, video_codec = symbols('format audio_codec resolution video_codec') hearing_impaired = symbols('hearing_impaired') + foreign_only = symbols('foreign_only') equations = [ # hash is best @@ -224,11 +236,14 @@ def solve_movie_equations(): Eq(resolution, video_codec), # video_codec is the least valuable match but counts more than the sum of all scoring increasing matches - Eq(video_codec, hearing_impaired + 1), + Eq(video_codec, (hearing_impaired + foreign_only) + 1), # hearing impaired is only used for score increasing, so put it to 1 Eq(hearing_impaired, 1), + + # foreign only is only used for score increasing, so put it to 1 + Eq(foreign_only, 1), ] - return solve(equations, [hash, title, year, release_group, format, audio_codec, resolution, hearing_impaired, + return solve(equations, [hash, title, year, release_group, format, audio_codec, resolution, hearing_impaired, foreign_only, video_codec]) diff --git a/subliminal/subtitle.py b/subliminal/subtitle.py index 60cdf3d6e..c96baf3ad 100644 --- a/subliminal/subtitle.py +++ b/subliminal/subtitle.py @@ -23,6 +23,7 @@ class Subtitle(object): :param language: language of the subtitle. :type language: :class:`~babelfish.language.Language` :param bool hearing_impaired: whether or not the subtitle is hearing impaired. + :param bool foreign_only: whether or not the subtitle is foreign parts only. :param page_link: URL of the web page from which the subtitle can be downloaded. :type page_link: str :param encoding: Text encoding of the subtitle. @@ -32,13 +33,16 @@ class Subtitle(object): #: Name of the provider that returns that class of subtitle provider_name = '' - def __init__(self, language, hearing_impaired=False, page_link=None, encoding=None): + def __init__(self, language, hearing_impaired=False, foreign_only=False, page_link=None, encoding=None): #: Language of the subtitle self.language = language #: Whether or not the subtitle is hearing impaired self.hearing_impaired = hearing_impaired + #: Whether or not the subtitle is foreign parts only + self.foreign_only = foreign_only + #: URL of the web page from which the subtitle can be downloaded self.page_link = page_link @@ -163,13 +167,14 @@ def __repr__(self): return '<%s %r [%s]>' % (self.__class__.__name__, self.id, self.language) -def get_subtitle_path(video_path, language=None, extension='.srt'): +def get_subtitle_path(video_path, language=None, forced=False, extension='.srt'): """Get the subtitle path using the `video_path` and `language`. :param str video_path: path to the video. :param language: language of the subtitle to put in the path. :type language: :class:`~babelfish.language.Language` :param str extension: extension of the subtitle. + :param forced: is this a forced subtitle file :return: path of the subtitle. :rtype: str @@ -179,6 +184,9 @@ def get_subtitle_path(video_path, language=None, extension='.srt'): if language: subtitle_root += '.' + str(language) + if forced: + subtitle_root += '.forced' + return subtitle_root + extension