subliminal-1.1.1/0000775000175000017500000000000012642175070014773 5ustar antoineantoine00000000000000subliminal-1.1.1/setup.py0000664000175000017500000000656112640606111016506 0ustar antoineantoine00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import io import re import sys from setuptools import setup, find_packages # requirements setup_requirements = ['pytest-runner'] if {'pytest', 'test', 'ptr'}.intersection(sys.argv) else [] install_requirements = ['guessit>=0.9.1,<2.0', 'babelfish>=0.5.2', 'enzyme>=0.4.1', 'beautifulsoup4>=4.2.0', 'requests>=2.0', 'click>=4.0', 'dogpile.cache>=0.5.4', 'stevedore>=1.0.0', 'chardet>=2.3.0', 'pysrt>=1.0.1', 'six>=1.9.0'] test_requirements = ['sympy', 'vcrpy>=1.6.1', 'pytest', 'pytest-pep8', 'pytest-flakes', 'pytest-cov'] if sys.version_info < (3, 3): test_requirements.append('mock') dev_requirements = ['tox', 'sphinx', 'transifex-client', 'wheel'] # package informations with io.open('subliminal/__init__.py', 'r') as f: version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]$', f.read(), re.MULTILINE).group(1) if not version: raise RuntimeError('Cannot find version information') with io.open('README.rst', 'r', encoding='utf-8') as f: readme = f.read() with io.open('HISTORY.rst', 'r', encoding='utf-8') as f: history = f.read() setup(name='subliminal', version=version, license='MIT', description='Subtitles, faster than your thoughts', long_description=readme + '\n\n' + history, keywords='subtitle subtitles video movie episode tv show', url='https://github.com/Diaoul/subliminal', author='Antoine Bertin', author_email='diaoulael@gmail.com', packages=find_packages(), classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Multimedia :: Video' ], entry_points={ 'subliminal.providers': [ 'addic7ed = subliminal.providers.addic7ed:Addic7edProvider', 'opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider', 'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider', 'subscenter = subliminal.providers.subscenter:SubsCenterProvider', 'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider', 'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider' ], 'babelfish.language_converters': [ 'addic7ed = subliminal.converters.addic7ed:Addic7edConverter', 'thesubdb = subliminal.converters.thesubdb:TheSubDBConverter', 'tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter' ], 'console_scripts': [ 'subliminal = subliminal.cli:subliminal' ] }, setup_requires=setup_requirements, install_requires=install_requirements, tests_require=test_requirements, extras_require={ 'test': test_requirements, 'dev': dev_requirements }) subliminal-1.1.1/requirements.txt0000664000175000017500000000000512601751321020244 0ustar antoineantoine00000000000000-e . subliminal-1.1.1/PKG-INFO0000664000175000017500000002661412642175070016101 0ustar antoineantoine00000000000000Metadata-Version: 1.1 Name: subliminal Version: 1.1.1 Summary: Subtitles, faster than your thoughts Home-page: https://github.com/Diaoul/subliminal Author: Antoine Bertin Author-email: diaoulael@gmail.com License: MIT Description: Subliminal ========== Subtitles, faster than your thoughts. .. image:: https://img.shields.io/pypi/v/subliminal.svg :target: https://pypi.python.org/pypi/subliminal :alt: Latest Version .. image:: https://travis-ci.org/Diaoul/subliminal.svg?branch=master :target: https://travis-ci.org/Diaoul/subliminal :alt: Travis CI build status .. image:: https://readthedocs.org/projects/subliminal/badge/?version=latest :target: https://subliminal.readthedocs.org/ :alt: Documentation Status .. image:: https://coveralls.io/repos/Diaoul/subliminal/badge.svg?branch=master&service=github :target: https://coveralls.io/github/Diaoul/subliminal?branch=master :alt: Code coverage .. image:: https://img.shields.io/github/license/Diaoul/subliminal.svg :target: https://github.com/Diaoul/subliminal/blob/master/LICENSE :alt: License .. image:: https://img.shields.io/badge/gitter-join%20chat-1dce73.svg :alt: Join the chat at https://gitter.im/Diaoul/subliminal :target: https://gitter.im/Diaoul/subliminal :Project page: https://github.com/Diaoul/subliminal :Documentation: https://subliminal.readthedocs.org/ Usage ----- CLI ^^^ Download English subtitles:: $ subliminal download -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4 Collecting videos [####################################] 100% 1 video collected / 0 video ignored / 0 error Downloading subtitles [####################################] 100% Downloaded 1 subtitle Library ^^^^^^^ Download best subtitles in French and English for videos less than two weeks old in a video folder: .. code:: python from datetime import timedelta from babelfish import Language from subliminal import download_best_subtitles, region, save_subtitles, scan_videos # configure the cache region.configure('dogpile.cache.dbm', arguments={'filename': 'cachefile.dbm'}) # scan for videos newer than 2 weeks and their existing subtitles in a folder videos = [v for v in scan_videos('/video/folder') if v.age < timedelta(weeks=2)] # download best subtitles subtitles = download_best_subtitles(videos, {Language('eng'), Language('fra')}) # save them to disk, next to the video for v in videos: save_subtitles(v, subtitles[v]) Nautilus integration -------------------- Screenshots ^^^^^^^^^^^ .. image:: http://i.imgur.com/NCwELpB.png :alt: Menu .. image:: http://i.imgur.com/Y58ky88.png :alt: Configuration .. image:: http://i.imgur.com/qem3DGj.png :alt: Choose subtitles Install ^^^^^^^ 1. Install subliminal on your system ``sudo pip install -U subliminal`` 2. Install nautilus-python with your package manager ``sudo apt-get install nautilus-python`` 3. Create the extension directory ``mkdir -p ~/.local/share/nautilus-python/extensions/subliminal`` 4. Copy the script ``cp examples/nautilus.py ~/.local/share/nautilus-python/extensions/subliminal-nautilus.py`` 5. Copy UI files ``cp -R examples/ui ~/.local/share/nautilus-python/extensions/subliminal/`` 6. (Optional) Create a translation directory for your language ``mkdir -p ~/.local/share/nautilus-python/extensions/subliminal/locale/fr/LC_MESSAGES`` 7. (Optional) Install the translation ``msgfmt examples/i18n/fr.po -o ~/.local/share/nautilus-python/extensions/subliminal/locale/fr/LC_MESSAGES/subliminal.mo`` Changelog --------- 1.1.1 ^^^^^ **release date:** 2016-01-03 * Fix scanning videos on bad MKV files 1.1 ^^^ **release date:** 2015-12-29 * Fix library usage example in README * Fix for series name with special characters in addic7ed provider * Fix id property in thesubdb provider * Improve matching on titles * Add support for nautilus context menu with translations * Add support for searching subtitles in a separate directory * Add subscenter provider * Add support for python 3.5 1.0.1 ^^^^^ **release date:** 2015-07-23 * Fix unicode issues in CLI (python 2 only) * Fix score scaling in CLI (python 2 only) * Improve error handling in CLI * Color collect report in CLI 1.0 ^^^ **release date:** 2015-07-22 * Many changes and fixes * New test suite * New documentation * New CLI * Added support for SubsCenter 0.7.5 ^^^^^ **release date:** 2015-03-04 * Update requirements * Remove BierDopje provider * Add pre-guessed video optional argument in scan_video * Improve hearing impaired support * Fix TVSubtitles and Podnapisi providers 0.7.4 ^^^^^ **release date:** 2014-01-27 * Fix requirements for guessit and babelfish 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 **WARNING:** Complete rewrite of subliminal with backward incompatible changes * Use enzyme to parse metadata of videos * Use babelfish to handle languages * Use dogpile.cache for caching * Use charade to detect subtitle encoding * Use pysrt for subtitle validation * Use entry points for subtitle providers * New subtitle score computation * Hearing impaired subtitles support * Drop async support * 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 * Fix BierDopje * Fix Addic7ed * Fix SubsWiki * Fix missing enzyme import * Add Catalan and Galician languages to Addic7ed * Add possible services in help message of the CLI * Allow existing filenames to be passed without the ./ prefix 0.6.1 ^^^^^ **release date:** 2012-06-24 * Fix subtitle release name in BierDopje * Fix subtitles being downloaded multiple times * Add Chinese support to TvSubtitles * Fix encoding issues * Fix single download subtitles without the force option * Add Spanish (Latin America) exception to Addic7ed * Fix group_by_video when a list entry has None as subtitles * Add support for Galician language in Subtitulos * Add an integrity check after subtitles download for Addic7ed * Add error handling for if not strict in Language * Fix TheSubDB hash method to return None if the file is too small * Fix guessit.Language in Video.scan * Fix language detection of subtitles 0.6.0 ^^^^^ **release date:** 2012-06-16 **WARNING:** Backward incompatible changes * Fix --workers option in CLI * Use a dedicated module for languages * Use beautifulsoup4 * Improve return types * Add scan_filter option * Add --age option in CLI * Add TvSubtitles service * Add Addic7ed service 0.5.1 ^^^^^ **release date:** 2012-03-25 * Improve error handling of enzyme parsing 0.5 ^^^ **release date:** 2012-03-25 **WARNING:** Backward incompatible changes * Use more unicode * New list_subtitles and download_subtitles methods * New Pool object for asynchronous work * Improve sort algorithm * Better error handling * Make sorting customizable * Remove class Subliminal * Remove permissions handling 0.4 ^^^ **release date:** 2011-11-11 * Many fixes * Better error handling 0.3 ^^^ **release date:** 2011-08-18 * Fix a bug when series is not guessed by guessit * Fix dependencies failure when installing package * Fix encoding issues with logging * Add a script to ease subtitles download * Add possibility to choose mode of created files * Add more checks before adjusting permissions 0.2 ^^^ **release date:** 2011-07-11 * Fix plugin configuration * Fix some encoding issues * Remove extra logging 0.1 ^^^ **release date:** *private release* * Initial release Keywords: subtitle subtitles video movie episode tv show Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Multimedia :: Video subliminal-1.1.1/HISTORY.rst0000664000175000017500000001107512642175051016671 0ustar antoineantoine00000000000000Changelog --------- 1.1.1 ^^^^^ **release date:** 2016-01-03 * Fix scanning videos on bad MKV files 1.1 ^^^ **release date:** 2015-12-29 * Fix library usage example in README * Fix for series name with special characters in addic7ed provider * Fix id property in thesubdb provider * Improve matching on titles * Add support for nautilus context menu with translations * Add support for searching subtitles in a separate directory * Add subscenter provider * Add support for python 3.5 1.0.1 ^^^^^ **release date:** 2015-07-23 * Fix unicode issues in CLI (python 2 only) * Fix score scaling in CLI (python 2 only) * Improve error handling in CLI * Color collect report in CLI 1.0 ^^^ **release date:** 2015-07-22 * Many changes and fixes * New test suite * New documentation * New CLI * Added support for SubsCenter 0.7.5 ^^^^^ **release date:** 2015-03-04 * Update requirements * Remove BierDopje provider * Add pre-guessed video optional argument in scan_video * Improve hearing impaired support * Fix TVSubtitles and Podnapisi providers 0.7.4 ^^^^^ **release date:** 2014-01-27 * Fix requirements for guessit and babelfish 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 **WARNING:** Complete rewrite of subliminal with backward incompatible changes * Use enzyme to parse metadata of videos * Use babelfish to handle languages * Use dogpile.cache for caching * Use charade to detect subtitle encoding * Use pysrt for subtitle validation * Use entry points for subtitle providers * New subtitle score computation * Hearing impaired subtitles support * Drop async support * 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 * Fix BierDopje * Fix Addic7ed * Fix SubsWiki * Fix missing enzyme import * Add Catalan and Galician languages to Addic7ed * Add possible services in help message of the CLI * Allow existing filenames to be passed without the ./ prefix 0.6.1 ^^^^^ **release date:** 2012-06-24 * Fix subtitle release name in BierDopje * Fix subtitles being downloaded multiple times * Add Chinese support to TvSubtitles * Fix encoding issues * Fix single download subtitles without the force option * Add Spanish (Latin America) exception to Addic7ed * Fix group_by_video when a list entry has None as subtitles * Add support for Galician language in Subtitulos * Add an integrity check after subtitles download for Addic7ed * Add error handling for if not strict in Language * Fix TheSubDB hash method to return None if the file is too small * Fix guessit.Language in Video.scan * Fix language detection of subtitles 0.6.0 ^^^^^ **release date:** 2012-06-16 **WARNING:** Backward incompatible changes * Fix --workers option in CLI * Use a dedicated module for languages * Use beautifulsoup4 * Improve return types * Add scan_filter option * Add --age option in CLI * Add TvSubtitles service * Add Addic7ed service 0.5.1 ^^^^^ **release date:** 2012-03-25 * Improve error handling of enzyme parsing 0.5 ^^^ **release date:** 2012-03-25 **WARNING:** Backward incompatible changes * Use more unicode * New list_subtitles and download_subtitles methods * New Pool object for asynchronous work * Improve sort algorithm * Better error handling * Make sorting customizable * Remove class Subliminal * Remove permissions handling 0.4 ^^^ **release date:** 2011-11-11 * Many fixes * Better error handling 0.3 ^^^ **release date:** 2011-08-18 * Fix a bug when series is not guessed by guessit * Fix dependencies failure when installing package * Fix encoding issues with logging * Add a script to ease subtitles download * Add possibility to choose mode of created files * Add more checks before adjusting permissions 0.2 ^^^ **release date:** 2011-07-11 * Fix plugin configuration * Fix some encoding issues * Remove extra logging 0.1 ^^^ **release date:** *private release* * Initial release subliminal-1.1.1/subliminal/0000775000175000017500000000000012642175070017132 5ustar antoineantoine00000000000000subliminal-1.1.1/subliminal/cache.py0000664000175000017500000000055712601751321020550 0ustar antoineantoine00000000000000# -*- coding: utf-8 -*- import datetime from dogpile.cache import make_region #: Subliminal's cache version CACHE_VERSION = 1 #: Expiration time for show caching SHOW_EXPIRATION_TIME = datetime.timedelta(weeks=3).total_seconds() #: Expiration time for episode caching EPISODE_EXPIRATION_TIME = datetime.timedelta(days=3).total_seconds() region = make_region() subliminal-1.1.1/subliminal/subtitle.py0000664000175000017500000002573412640425355021354 0ustar antoineantoine00000000000000# -*- coding: utf-8 -*- import logging import os import re import chardet from guessit.matchtree import MatchTree from guessit.plugins.transformers import get_transformer from codecs import lookup import pysrt from .video import Episode, Movie logger = logging.getLogger(__name__) class Subtitle(object): """Base class for subtitle. :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 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 :type encoding: str """ #: Name of the provider that returns that class of subtitle provider_name = '' def __init__(self, language, hearing_impaired=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 #: URL of the web page from which the subtitle can be downloaded self.page_link = page_link #: Content as bytes self.content = None #: Encoding to decode with when accessing :attr:`text` if encoding: try: # set encoding to canonical codec name self.encoding = lookup(encoding).name except (TypeError, LookupError): logger.debug('Unsupported encoding "%s", setting to None', encoding) self.encoding = None else: self.encoding = None @property def id(self): """Unique identifier of the subtitle.""" raise NotImplementedError @property def text(self): """Content as string. If :attr:`encoding` is None, the encoding is guessed with :meth:`guess_encoding` """ if not self.content: return try: return self.content.decode(self.encoding, errors='replace') except (TypeError, LookupError): # Failback to guess_encoding if empty or unknown encoding provided return self.content.decode(self.guess_encoding(), errors='replace') def is_valid(self): """Check if a :attr:`text` is a valid SubRip format. :return: whether or not the subtitle is valid. :rtype: bool """ if not self.text: return False try: pysrt.from_string(self.text, error_handling=pysrt.ERROR_RAISE) except pysrt.Error as e: if e.args[0] < 80: return False return True def guess_encoding(self): """Guess encoding using the language, falling back on chardet. :return: the guessed encoding. :rtype: str """ logger.info('Guessing encoding for language %s', self.language) # always try utf-8 first encodings = ['utf-8'] # add language-specific encodings if self.language.alpha3 == 'zho': encodings.extend(['gb18030', 'big5']) elif self.language.alpha3 == 'jpn': encodings.append('shift-jis') elif self.language.alpha3 == 'ara': encodings.append('windows-1256') elif self.language.alpha3 == 'heb': encodings.append('windows-1255') elif self.language.alpha3 == 'tur': encodings.extend(['iso-8859-9', 'windows-1254']) elif self.language.alpha3 == 'pol': # Eastern European Group 1 encodings.extend(['windows-1250']) elif self.language.alpha3 == 'bul': # Eastern European Group 2 encodings.extend(['windows-1251']) else: # Western European (windows-1252) encodings.append('latin-1') # try to decode logger.debug('Trying encodings %r', encodings) for encoding in encodings: try: self.content.decode(encoding) except UnicodeDecodeError: pass else: logger.info('Guessed encoding %s', encoding) return encoding logger.warning('Could not guess encoding from language') # fallback on chardet encoding = chardet.detect(self.content)['encoding'] logger.info('Chardet found encoding %s', encoding) return encoding def get_matches(self, video, hearing_impaired=False): """Get the matches against the `video`. :param video: the video to get the matches with. :type video: :class:`~subliminal.video.Video` :param bool hearing_impaired: hearing impaired preference. :return: matches of the subtitle. :rtype: set """ matches = set() # hearing_impaired if self.hearing_impaired == hearing_impaired: matches.add('hearing_impaired') return matches def __hash__(self): return hash(self.provider_name + '-' + self.id) def __repr__(self): return '<%s %r [%s]>' % (self.__class__.__name__, self.id, self.language) def compute_score(matches, video, scores=None): """Compute the score of the `matches` against the `video`. Some matches count as much as a combination of others in order to level the final score: * `hash` removes everything else * For :class:`~subliminal.video.Episode` * `imdb_id` removes `series`, `tvdb_id`, `season`, `episode`, `title` and `year` * `tvdb_id` removes `series` and `year` * `title` removes `season` and `episode` :param video: the video to get the score with. :type video: :class:`~subliminal.video.Video` :param dict scores: scores to use, if `None`, the :attr:`~subliminal.video.Video.scores` from the video are used. :return: score of the subtitle. :rtype: int """ final_matches = matches.copy() scores = scores or video.scores logger.info('Computing score for matches %r and %r', matches, video) # remove equivalent match combinations if 'hash' in final_matches: final_matches &= {'hash', 'hearing_impaired'} elif isinstance(video, Episode): if 'imdb_id' in final_matches: final_matches -= {'series', 'tvdb_id', 'season', 'episode', 'title', 'year'} if 'tvdb_id' in final_matches: final_matches -= {'series', 'year'} if 'title' in final_matches: final_matches -= {'season', 'episode'} # compute score logger.debug('Final matches: %r', final_matches) score = sum((scores[match] for match in final_matches)) logger.info('Computed score %d', score) # ensure score is capped by the best possible score (hash + preferences) assert score <= scores['hash'] + scores['hearing_impaired'] return score def get_subtitle_path(video_path, language=None, 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. :return: path of the subtitle. :rtype: str """ subtitle_root = os.path.splitext(video_path)[0] if language: subtitle_root += '.' + str(language) return subtitle_root + extension def sanitize_string(string, replacement=''): """Replace any special characters from a string. :param str string: the string to sanitize. :param str replacement: the replacement for special characters. :return: the sanitized string. :rtype: str """ return re.sub('[^ a-zA-Z0-9]', replacement, string) def sanitized_string_equal(string1, string2): """Test two strings for equality case insensitively and ignoring special characters. :param str string1: the first string to compare. :param str string2: the second string to compare. :return: `True` if the two strings are equal, `False` otherwise. :rtype: bool """ return string1 and string2 and sanitize_string(string1).lower() == sanitize_string(string2).lower() def guess_matches(video, guess, partial=False): """Get matches between a `video` and a `guess`. If a guess is `partial`, the absence information won't be counted as a match. :param video: the video. :type video: :class:`~subliminal.video.Video` :param guess: the guess. :type guess: dict :param bool partial: whether or not the guess is partial. :return: matches between the `video` and the `guess`. :rtype: set """ matches = set() if isinstance(video, Episode): # series if video.series and 'series' in guess and sanitized_string_equal(guess['series'], video.series): matches.add('series') # season if video.season and 'season' in guess and guess['season'] == video.season: matches.add('season') # episode if video.episode and 'episodeNumber' in guess and guess['episodeNumber'] == video.episode: matches.add('episode') # year if video.year and 'year' in guess and guess['year'] == video.year: matches.add('year') # count "no year" as an information if not partial and video.year is None and 'year' not in guess: matches.add('year') elif isinstance(video, Movie): # year if video.year and 'year' in guess and guess['year'] == video.year: matches.add('year') # title if video.title and 'title' in guess and sanitized_string_equal(guess['title'], video.title): matches.add('title') # release_group if video.release_group and 'releaseGroup' in guess and guess['releaseGroup'].lower() == video.release_group.lower(): matches.add('release_group') # resolution if video.resolution and 'screenSize' in guess and guess['screenSize'] == video.resolution: matches.add('resolution') # format if video.format and 'format' in guess and guess['format'].lower() == video.format.lower(): matches.add('format') # video_codec if video.video_codec and 'videoCodec' in guess and guess['videoCodec'] == video.video_codec: matches.add('video_codec') # audio_codec if video.audio_codec and 'audioCodec' in guess and guess['audioCodec'] == video.audio_codec: matches.add('audio_codec') return matches def guess_properties(string): """Extract properties from `string` using guessit's `guess_properties` transformer. :param str string: the string potentially containing properties. :return: the guessed properties. :rtype: dict """ mtree = MatchTree(string) get_transformer('guess_properties').process(mtree) return mtree.matched() def fix_line_ending(content): """Fix line ending of `content` by changing it to \n. :param bytes content: content of the subtitle. :return: the content with fixed line endings. :rtype: bytes """ return content.replace(b'\r\n', b'\n').replace(b'\r', b'\n') subliminal-1.1.1/subliminal/video.py0000664000175000017500000004565412642173311020624 0ustar antoineantoine00000000000000# -*- coding: utf-8 -*- from __future__ import division from datetime import datetime, timedelta import hashlib import logging import os import struct from babelfish import Error as BabelfishError, Language from enzyme import Error as EnzymeError, MKV from guessit import guess_episode_info, guess_file_info, guess_movie_info logger = logging.getLogger(__name__) #: Video extensions VIDEO_EXTENSIONS = ('.3g2', '.3gp', '.3gp2', '.3gpp', '.60d', '.ajp', '.asf', '.asx', '.avchd', '.avi', '.bik', '.bix', '.box', '.cam', '.dat', '.divx', '.dmf', '.dv', '.dvr-ms', '.evo', '.flc', '.fli', '.flic', '.flv', '.flx', '.gvi', '.gvp', '.h264', '.m1v', '.m2p', '.m2ts', '.m2v', '.m4e', '.m4v', '.mjp', '.mjpeg', '.mjpg', '.mkv', '.moov', '.mov', '.movhd', '.movie', '.movx', '.mp4', '.mpe', '.mpeg', '.mpg', '.mpv', '.mpv2', '.mxf', '.nsv', '.nut', '.ogg', '.ogm', '.omf', '.ps', '.qt', '.ram', '.rm', '.rmvb', '.swf', '.ts', '.vfw', '.vid', '.video', '.viv', '.vivo', '.vob', '.vro', '.wm', '.wmv', '.wmx', '.wrap', '.wvx', '.wx', '.x264', '.xvid') #: Subtitle extensions SUBTITLE_EXTENSIONS = ('.srt', '.sub', '.smi', '.txt', '.ssa', '.ass', '.mpl') class Video(object): """Base class for videos. Represent a video, existing or not. Attributes have an associated score based on equations defined in :mod:`~subliminal.score`. :param str name: name or path of the video. :param str format: format of the video (HDTV, WEB-DL, BluRay, ...). :param str release_group: release group of the video. :param str resolution: resolution of the video stream (480p, 720p, 1080p or 1080i). :param str video_codec: codec of the video stream. :param str audio_codec: codec of the main audio stream. :param int imdb_id: IMDb id of the video. :param dict hashes: hashes of the video file by provider names. :param int size: size of the video file in bytes. :param set subtitle_languages: existing subtitle languages """ #: Score by match property scores = {} def __init__(self, name, format=None, release_group=None, resolution=None, video_codec=None, audio_codec=None, imdb_id=None, hashes=None, size=None, subtitle_languages=None): #: Name or path of the video self.name = name #: Format of the video (HDTV, WEB-DL, BluRay, ...) self.format = format #: Release group of the video self.release_group = release_group #: Resolution of the video stream (480p, 720p, 1080p or 1080i) self.resolution = resolution #: Codec of the video stream self.video_codec = video_codec #: Codec of the main audio stream self.audio_codec = audio_codec #: IMDb id of the video self.imdb_id = imdb_id #: Hashes of the video file by provider names self.hashes = hashes or {} #: Size of the video file in bytes self.size = size #: Existing subtitle languages self.subtitle_languages = subtitle_languages or set() @property def exists(self): """Test whether the video exists.""" return os.path.exists(self.name) @property def age(self): """Age of the video.""" if self.exists: return datetime.utcnow() - datetime.utcfromtimestamp(os.path.getmtime(self.name)) return timedelta() @classmethod def fromguess(cls, name, guess): """Create an :class:`Episode` or a :class:`Movie` with the given `name` based on the `guess`. :param str name: name of the video. :param dict guess: guessed data, like a :class:`~guessit.guess.Guess` instance. :raise: :class:`ValueError` if the `type` of the `guess` is invalid """ if guess['type'] == 'episode': return Episode.fromguess(name, guess) if guess['type'] == 'movie': return Movie.fromguess(name, guess) raise ValueError('The guess must be an episode or a movie guess') @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): return '<%s [%r]>' % (self.__class__.__name__, self.name) def __hash__(self): return hash(self.name) class Episode(Video): """Episode :class:`Video`. Scores are defined by a set of equations, see :func:`~subliminal.score.solve_episode_equations` :param str series: series of the episode. :param int season: season number of the episode. :param int episode: episode number of the episode. :param str title: title of the episode. :param int year: year of series. :param int tvdb_id: TVDB id of the episode """ #: Score by match property scores = {'hash': 137, 'imdb_id': 110, 'tvdb_id': 88, 'series': 44, 'year': 44, 'title': 22, 'season': 11, 'episode': 11, 'release_group': 11, 'format': 6, 'video_codec': 4, 'resolution': 4, 'audio_codec': 2, 'hearing_impaired': 1} def __init__(self, name, series, season, episode, format=None, release_group=None, resolution=None, video_codec=None, audio_codec=None, imdb_id=None, hashes=None, size=None, subtitle_languages=None, title=None, year=None, tvdb_id=None): super(Episode, self).__init__(name, format, release_group, resolution, video_codec, audio_codec, imdb_id, hashes, size, subtitle_languages) #: Series of the episode self.series = series #: Season number of the episode self.season = season #: Episode number of the episode self.episode = episode #: Title of the episode self.title = title #: Year of series self.year = year #: TVDB id of the episode self.tvdb_id = tvdb_id @classmethod def fromguess(cls, name, guess): if guess['type'] != 'episode': raise ValueError('The guess must be an episode guess') if 'series' not in guess or 'season' not in guess or 'episodeNumber' not in guess: raise ValueError('Insufficient data to process the guess') return cls(name, guess['series'], guess['season'], guess['episodeNumber'], format=guess.get('format'), release_group=guess.get('releaseGroup'), resolution=guess.get('screenSize'), video_codec=guess.get('videoCodec'), audio_codec=guess.get('audioCodec'), title=guess.get('title'), year=guess.get('year')) @classmethod def fromname(cls, name): return cls.fromguess(name, guess_episode_info(name)) def __repr__(self): if self.year is None: return '<%s [%r, %dx%d]>' % (self.__class__.__name__, self.series, self.season, self.episode) return '<%s [%r, %d, %dx%d]>' % (self.__class__.__name__, self.series, self.year, self.season, self.episode) class Movie(Video): """Movie :class:`Video`. Scores are defined by a set of equations, see :func:`~subliminal.score.solve_movie_equations` :param str title: title of the movie. :param int year: year of the movie """ #: Score by match property scores = {'hash': 62, 'imdb_id': 62, 'title': 23, 'year': 12, 'release_group': 11, 'format': 6, 'video_codec': 4, 'resolution': 4, 'audio_codec': 2, 'hearing_impaired': 1} def __init__(self, name, title, format=None, release_group=None, resolution=None, video_codec=None, audio_codec=None, imdb_id=None, hashes=None, size=None, subtitle_languages=None, year=None): super(Movie, self).__init__(name, format, release_group, resolution, video_codec, audio_codec, imdb_id, hashes, size, subtitle_languages) #: Title of the movie self.title = title #: Year of the movie self.year = year @classmethod def fromguess(cls, name, guess): if guess['type'] != 'movie': raise ValueError('The guess must be a movie guess') if 'title' not in guess: raise ValueError('Insufficient data to process the guess') return cls(name, guess['title'], format=guess.get('format'), release_group=guess.get('releaseGroup'), resolution=guess.get('screenSize'), video_codec=guess.get('videoCodec'), audio_codec=guess.get('audioCodec'), year=guess.get('year')) @classmethod def fromname(cls, name): return cls.fromguess(name, guess_movie_info(name)) def __repr__(self): if self.year is None: return '<%s [%r]>' % (self.__class__.__name__, self.title) return '<%s [%r, %d]>' % (self.__class__.__name__, self.title, self.year) def search_external_subtitles(path, directory=None): """Search for external subtitles from a video `path` and their associated language. Unless `directory` is provided, search will be made in the same directory as the video file. :param str path: path to the video. :param str directory: directory to search for subtitles. :return: found subtitles with their languages. :rtype: dict """ dirpath, filename = os.path.split(path) dirpath = dirpath or '.' fileroot, fileext = os.path.splitext(filename) subtitles = {} for p in os.listdir(directory or dirpath): # keep only valid subtitle filenames if not p.startswith(fileroot) or not p.endswith(SUBTITLE_EXTENSIONS): continue # extract the potential language code language_code = p[len(fileroot):-len(os.path.splitext(p)[1])].replace(fileext, '').replace('_', '-')[1:] # default language is undefined language = Language('und') # attempt to parse if language_code: try: language = Language.fromietf(language_code) except ValueError: logger.error('Cannot parse language code %r', language_code) subtitles[p] = language logger.debug('Found subtitles %r', subtitles) return subtitles def scan_video(path, subtitles=True, embedded_subtitles=True, subtitles_dir=None): """Scan a video and its subtitle languages from a video `path`. :param str path: existing path to the video. :param bool subtitles: scan for subtitles with the same name. :param bool embedded_subtitles: scan for embedded subtitles. :param str subtitles_dir: directory to search for subtitles. :return: the scanned video. :rtype: :class:`Video` """ # check for non-existing path if not os.path.exists(path): raise ValueError('Path does not exist') # check video extension if not path.endswith(VIDEO_EXTENSIONS): raise ValueError('%s is not a valid video extension' % os.path.splitext(path)[1]) dirpath, filename = os.path.split(path) logger.info('Scanning video %r in %r', filename, dirpath) # guess video = Video.fromguess(path, guess_file_info(path)) # size and hashes 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) video.hashes['napiprojekt'] = hash_napiprojekt(path) logger.debug('Computed hashes %r', video.hashes) else: logger.warning('Size is lower than 10MB: hashes not computed') # external subtitles if subtitles: video.subtitle_languages |= set(search_external_subtitles(path, directory=subtitles_dir).values()) # video metadata with enzyme try: if filename.endswith('.mkv'): with open(path, 'rb') as f: mkv = MKV(f) # main video track 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 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') # main audio 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') # subtitle tracks if mkv.subtitle_tracks: if embedded_subtitles: embedded_subtitle_languages = set() for st in mkv.subtitle_tracks: if st.language: try: embedded_subtitle_languages.add(Language.fromalpha3b(st.language)) except BabelfishError: logger.error('Embedded subtitle track language %r is not a valid language', st.language) embedded_subtitle_languages.add(Language('und')) elif st.name: try: embedded_subtitle_languages.add(Language.fromname(st.name)) except BabelfishError: logger.debug('Embedded subtitle track name %r is not a valid language', st.name) embedded_subtitle_languages.add(Language('und')) else: embedded_subtitle_languages.add(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: logger.exception('Parsing video metadata with enzyme failed') return video def scan_videos(path, subtitles=True, embedded_subtitles=True, subtitles_dir=None): """Scan `path` for videos and their subtitles. :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. :param str subtitles_dir: directory to search for subtitles. :return: the scanned videos. :rtype: list of :class:`Video` """ # check for non-existing path if not os.path.exists(path): raise ValueError('Path does not exist') # check for non-directory path if not os.path.isdir(path): raise ValueError('Path is not a directory') # walk the path videos = [] for dirpath, dirnames, filenames in os.walk(path): logger.debug('Walking directory %s', dirpath) # remove badly encoded and hidden dirnames for dirname in list(dirnames): if dirname.startswith('.'): logger.debug('Skipping hidden dirname %r in %r', dirname, dirpath) dirnames.remove(dirname) # scan for videos for filename in filenames: # filter on 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 # reconstruct the file path filepath = os.path.join(dirpath, filename) # skip links if os.path.islink(filepath): logger.debug('Skipping link %r in %r', filename, dirpath) continue # scan video try: video = scan_video(filepath, subtitles=subtitles, embedded_subtitles=embedded_subtitles, subtitles_dir=subtitles_dir) except ValueError: # pragma: no cover logger.exception('Error scanning video') continue videos.append(video) return videos def hash_opensubtitles(video_path): """Compute a hash using OpenSubtitles' algorithm. :param str video_path: path of the video. :return: the hash. :rtype: str """ bytesize = struct.calcsize(b'` and :attr:`Movie.scores `) by assigning a score to a match. .. note:: To avoid unnecessary dependency on `sympy `_ and boost subliminal's import time, the resulting scores are hardcoded in their respective classes and manually updated when the set of equations change. Available matches: * hearing_impaired * format * release_group * resolution * video_codec * audio_codec * imdb_id * hash * title * year * series * season * episode * tvdb_id The :meth:`Subtitle.get_matches ` method get the matches between the :class:`~subliminal.subtitle.Subtitle` and the :class:`~subliminal.video.Video` and :func:`~subliminal.subtitle.compute_score` computes the score. """ from __future__ import print_function from sympy import Eq, solve, symbols # Symbols hearing_impaired, format, release_group, resolution = symbols('hearing_impaired format release_group resolution') video_codec, audio_codec, imdb_id, hash, title, year = symbols('video_codec audio_codec imdb_id hash title year') series, season, episode, tvdb_id = symbols('series season episode tvdb_id') def solve_episode_equations(): """Solve the score equations for an :class:`~subliminal.video.Episode`. The equations are the following: 1. hash = resolution + format + video_codec + audio_codec + series + season + episode + year + release_group 2. series = resolution + video_codec + audio_codec + season + episode + release_group + 1 3. year = series 4. tvdb_id = series + year 5. season = resolution + video_codec + audio_codec + 1 6. imdb_id = series + season + episode + year 7. format = video_codec + audio_codec 8. resolution = video_codec 9. video_codec = 2 * audio_codec 10. title = season + episode 11. season = episode 12. release_group = season 13. audio_codec = 2 * hearing_impaired 14. hearing_impaired = 1 :return: the result of the equations. :rtype: dict """ equations = [ Eq(hash, resolution + format + video_codec + audio_codec + series + season + episode + year + release_group), Eq(series, resolution + video_codec + audio_codec + season + episode + release_group + 1), Eq(year, series), Eq(tvdb_id, series + year), Eq(season, resolution + video_codec + audio_codec + 1), Eq(imdb_id, series + season + episode + year), Eq(format, video_codec + audio_codec), Eq(resolution, video_codec), Eq(video_codec, 2 * audio_codec), Eq(title, season + episode), Eq(season, episode), Eq(release_group, season), Eq(audio_codec, 2 * hearing_impaired), Eq(hearing_impaired, 1) ] return solve(equations, [hearing_impaired, format, release_group, resolution, video_codec, audio_codec, imdb_id, hash, series, season, episode, title, year, tvdb_id]) def solve_movie_equations(): """Solve the score equations for a :class:`~subliminal.video.Movie`. The equations are the following: 1. hash = resolution + format + video_codec + audio_codec + title + year + release_group 2. imdb_id = hash 3. resolution = video_codec 4. video_codec = 2 * audio_codec 5. format = video_codec + audio_codec 6. title = resolution + video_codec + audio_codec + year + 1 7. release_group = resolution + video_codec + audio_codec + 1 8. year = release_group + 1 9. audio_codec = 2 * hearing_impaired 10. hearing_impaired = 1 :return: the result of the equations. :rtype: dict """ equations = [ Eq(hash, resolution + format + video_codec + audio_codec + title + year + release_group), Eq(imdb_id, hash), Eq(resolution, video_codec), Eq(video_codec, 2 * audio_codec), Eq(format, video_codec + audio_codec), Eq(title, resolution + video_codec + audio_codec + year + 1), Eq(release_group, resolution + video_codec + audio_codec + 1), Eq(year, release_group + 1), Eq(audio_codec, 2 * hearing_impaired), Eq(hearing_impaired, 1) ] return solve(equations, [hearing_impaired, format, release_group, resolution, video_codec, audio_codec, imdb_id, hash, title, year]) subliminal-1.1.1/subliminal/providers/0000775000175000017500000000000012642175070021147 5ustar antoineantoine00000000000000subliminal-1.1.1/subliminal/providers/napiprojekt.py0000664000175000017500000000540012640425355024050 0ustar antoineantoine00000000000000# -*- coding: utf-8 -*- import logging from babelfish import Language from requests import Session from . import Provider, get_version from .. import __version__ 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() self.session.headers = {'User-Agent': 'Subliminal/%s' % get_version(__version__)} 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 subliminal-1.1.1/subliminal/providers/opensubtitles.py0000664000175000017500000002372612640425355024435 0ustar antoineantoine00000000000000# -*- coding: utf-8 -*- import base64 import logging import os import re import zlib from babelfish import Language, language_converters from guessit import guess_episode_info, guess_movie_info from six.moves.xmlrpc_client import ServerProxy from . import Provider, TimeoutSafeTransport, get_version from .. import __version__ from ..exceptions import AuthenticationError, ConfigurationError, DownloadLimitExceeded, ProviderError from ..subtitle import Subtitle, fix_line_ending, guess_matches, sanitized_string_equal from ..video import Episode, Movie logger = logging.getLogger(__name__) class OpenSubtitlesSubtitle(Subtitle): provider_name = 'opensubtitles' series_re = re.compile('^"(?P.*)" (?P.*)$') def __init__(self, language, hearing_impaired, page_link, subtitle_id, matched_by, movie_kind, hash, movie_name, movie_release_name, movie_year, movie_imdb_id, series_season, series_episode, encoding): super(OpenSubtitlesSubtitle, self).__init__(language, hearing_impaired, page_link, encoding) self.subtitle_id = subtitle_id self.matched_by = matched_by self.movie_kind = movie_kind self.hash = hash self.movie_name = movie_name self.movie_release_name = movie_release_name self.movie_year = movie_year self.movie_imdb_id = movie_imdb_id self.series_season = series_season self.series_episode = series_episode @property def id(self): return str(self.subtitle_id) @property def series_name(self): return self.series_re.match(self.movie_name).group('series_name') @property def series_title(self): return self.series_re.match(self.movie_name).group('series_title') def get_matches(self, video, hearing_impaired=False): matches = super(OpenSubtitlesSubtitle, self).get_matches(video, hearing_impaired=hearing_impaired) # episode if isinstance(video, Episode) and self.movie_kind == 'episode': # series if video.series and sanitized_string_equal(self.series_name, video.series): matches.add('series') # season if video.season and self.series_season == video.season: matches.add('season') # episode if video.episode and self.series_episode == video.episode: matches.add('episode') # title if video.title and sanitized_string_equal(self.series_title, video.title): matches.add('title') # guess matches |= guess_matches(video, guess_episode_info(self.movie_release_name + '.mkv')) # movie elif isinstance(video, Movie) and self.movie_kind == 'movie': # title if video.title and sanitized_string_equal(self.movie_name, video.title): matches.add('title') # year if video.year and self.movie_year == video.year: matches.add('year') # guess matches |= guess_matches(video, guess_movie_info(self.movie_release_name + '.mkv')) else: logger.info('%r is not a valid movie_kind', self.movie_kind) return matches # hash if 'opensubtitles' in video.hashes and self.hash == video.hashes['opensubtitles']: matches.add('hash') # imdb_id if video.imdb_id and self.movie_imdb_id == video.imdb_id: matches.add('imdb_id') return matches class OpenSubtitlesProvider(Provider): languages = {Language.fromopensubtitles(l) for l in language_converters['opensubtitles'].codes} 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(self.username, self.password, 'eng', 'subliminal v%s' % get_version(__version__))) self.token = response['token'] logger.debug('Logged in with token %r', self.token) def terminate(self): logger.info('Logging out') checked(self.server.LogOut(self.token)) self.server.close() self.token = None logger.debug('Logged out') def no_operation(self): logger.debug('No operation') checked(self.server.NoOperation(self.token)) def query(self, languages, hash=None, size=None, imdb_id=None, query=None, season=None, episode=None): # fill the search criteria criteria = [] if hash and size: criteria.append({'moviehash': hash, 'moviebytesize': str(size)}) if imdb_id: criteria.append({'imdbid': imdb_id}) if query and season and episode: criteria.append({'query': query.replace('\'', ''), 'season': season, 'episode': episode}) elif query: criteria.append({'query': query.replace('\'', '')}) if not criteria: raise ValueError('Not enough information') # add the language for criterion in criteria: criterion['sublanguageid'] = ','.join(sorted(l.opensubtitles for l in languages)) # query the server logger.info('Searching subtitles %r', criteria) response = checked(self.server.SearchSubtitles(self.token, criteria)) subtitles = [] # exit if no data if not response['data']: logger.debug('No subtitles found') return subtitles # loop over subtitle items for subtitle_item in response['data']: # read the item language = Language.fromopensubtitles(subtitle_item['SubLanguageID']) hearing_impaired = bool(int(subtitle_item['SubHearingImpaired'])) page_link = subtitle_item['SubtitlesLink'] subtitle_id = int(subtitle_item['IDSubtitleFile']) matched_by = subtitle_item['MatchedBy'] movie_kind = subtitle_item['MovieKind'] hash = subtitle_item['MovieHash'] movie_name = subtitle_item['MovieName'] movie_release_name = subtitle_item['MovieReleaseName'] movie_year = int(subtitle_item['MovieYear']) if subtitle_item['MovieYear'] else None movie_imdb_id = int(subtitle_item['IDMovieImdb']) series_season = int(subtitle_item['SeriesSeason']) if subtitle_item['SeriesSeason'] else None series_episode = int(subtitle_item['SeriesEpisode']) if subtitle_item['SeriesEpisode'] else None encoding = subtitle_item.get('SubEncoding') or None subtitle = OpenSubtitlesSubtitle(language, hearing_impaired, page_link, subtitle_id, matched_by, movie_kind, hash, movie_name, movie_release_name, movie_year, movie_imdb_id, series_season, series_episode, encoding) logger.debug('Found subtitle %r', subtitle) subtitles.append(subtitle) return subtitles def list_subtitles(self, video, languages): query = season = episode = None if isinstance(video, Episode): query = video.series season = video.season episode = video.episode elif ('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, season=season, episode=episode) def download_subtitle(self, subtitle): logger.info('Downloading subtitle %r', subtitle) response = checked(self.server.DownloadSubtitles(self.token, [str(subtitle.subtitle_id)])) subtitle.content = fix_line_ending(zlib.decompress(base64.b64decode(response['data'][0]['data']), 47)) class OpenSubtitlesError(ProviderError): """Base class for non-generic :class:`OpenSubtitlesProvider` exceptions.""" pass class Unauthorized(OpenSubtitlesError, AuthenticationError): """Exception raised when status is '401 Unauthorized'.""" pass class NoSession(OpenSubtitlesError, AuthenticationError): """Exception raised when status is '406 No session'.""" pass class DownloadLimitReached(OpenSubtitlesError, DownloadLimitExceeded): """Exception raised when status is '407 Download limit reached'.""" pass class InvalidImdbid(OpenSubtitlesError): """Exception raised when status is '413 Invalid ImdbID'.""" pass class UnknownUserAgent(OpenSubtitlesError, AuthenticationError): """Exception raised when status is '414 Unknown User Agent'.""" pass class DisabledUserAgent(OpenSubtitlesError, AuthenticationError): """Exception raised when status is '415 Disabled user agent'.""" pass class ServiceUnavailable(OpenSubtitlesError): """Exception raised when status is '503 Service Unavailable'.""" pass def checked(response): """Check a response status before returning it. :param response: a response from a XMLRPC call to OpenSubtitles. :return: the response. :raise: :class:`OpenSubtitlesError` """ status_code = int(response['status'][:3]) if status_code == 401: raise Unauthorized if status_code == 406: raise NoSession if status_code == 407: raise DownloadLimitReached if status_code == 413: raise InvalidImdbid if status_code == 414: raise UnknownUserAgent if status_code == 415: raise DisabledUserAgent if status_code == 503: raise ServiceUnavailable if status_code != 200: raise OpenSubtitlesError(response['status']) return response subliminal-1.1.1/subliminal/providers/__init__.py0000664000175000017500000001247712640425355023275 0ustar antoineantoine00000000000000# -*- coding: utf-8 -*- import logging from bs4 import BeautifulSoup, FeatureNotFound from six.moves.xmlrpc_client import SafeTransport from ..video import Episode, Movie logger = logging.getLogger(__name__) def get_version(version): """Put the `version` in the major.minor form. :param str version: the full version. :return: the major.minor form of the `version`. :rtype: str """ return '.'.join(version.split('.')[:2]) class TimeoutSafeTransport(SafeTransport): """Timeout support for ``xmlrpc.client.SafeTransport``.""" def __init__(self, timeout, *args, **kwargs): SafeTransport.__init__(self, *args, **kwargs) self.timeout = timeout def make_connection(self, host): c = SafeTransport.make_connection(self, host) c.timeout = self.timeout return c class ParserBeautifulSoup(BeautifulSoup): """A ``bs4.BeautifulSoup`` that picks the first parser available in `parsers`. :param markup: markup for the ``bs4.BeautifulSoup``. :param list parsers: parser names, in order of preference """ def __init__(self, markup, parsers, **kwargs): # reject features if set(parsers).intersection({'fast', 'permissive', 'strict', 'xml', 'html', 'html5'}): raise ValueError('Features not allowed, only parser names') # reject some kwargs if 'features' in kwargs: raise ValueError('Cannot use features kwarg') if 'builder' in kwargs: raise ValueError('Cannot use builder kwarg') # pick the first parser available for parser in parsers: try: super(ParserBeautifulSoup, self).__init__(markup, parser, **kwargs) return except FeatureNotFound: pass raise FeatureNotFound class Provider(object): """Base class for providers. If any configuration is possible for the provider, like credentials, it must take place during instantiation. :raise: :class:`~subliminal.exceptions.ConfigurationError` if there is a configuration error """ #: Supported set of :class:`~babelfish.language.Language` languages = set() #: Supported video types video_types = (Episode, Movie) #: Required hash, if any required_hash = None def __enter__(self): self.initialize() return self def __exit__(self, exc_type, exc_value, traceback): self.terminate() def initialize(self): """Initialize the provider. Must be called when starting to work with the provider. This is the place for network initialization or login operations. .. note:: This is called automatically when entering the :keyword:`with` statement """ raise NotImplementedError def terminate(self): """Terminate the provider. Must be called when done with the provider. This is the place for network shutdown or logout operations. .. note:: This is called automatically when exiting the :keyword:`with` statement """ raise NotImplementedError @classmethod def check(cls, video): """Check if the `video` can be processed. The `video` is considered invalid if not an instance of :attr:`video_types` or if the :attr:`required_hash` is not present in :attr:`~subliminal.video.Video.hashes` attribute of the `video`. :param video: the video to check. :type video: :class:`~subliminal.video.Video` :return: `True` if the `video` is valid, `False` otherwise. :rtype: bool """ if not isinstance(video, cls.video_types): return False if cls.required_hash is not None and cls.required_hash not in video.hashes: return False return True def query(self, *args, **kwargs): """Query the provider for subtitles. Arguments should match as much as possible the actual parameters for querying the provider :return: found subtitles. :rtype: list of :class:`~subliminal.subtitle.Subtitle` :raise: :class:`~subliminal.exceptions.ProviderError` """ raise NotImplementedError def list_subtitles(self, video, languages): """List subtitles for the `video` with the given `languages`. This will call the :meth:`query` method internally. The parameters passed to the :meth:`query` method may vary depending on the amount of information available in the `video`. :param video: video to list subtitles for. :type video: :class:`~subliminal.video.Video` :param languages: languages to search for. :type languages: set of :class:`~babelfish.language.Language` :return: found subtitles. :rtype: list of :class:`~subliminal.subtitle.Subtitle` :raise: :class:`~subliminal.exceptions.ProviderError` """ raise NotImplementedError def download_subtitle(self, subtitle): """Download `subtitle`'s :attr:`~subliminal.subtitle.Subtitle.content`. :param subtitle: subtitle to download. :type subtitle: :class:`~subliminal.subtitle.Subtitle` :raise: :class:`~subliminal.exceptions.ProviderError` """ raise NotImplementedError def __repr__(self): return '<%s [%r]>' % (self.__class__.__name__, self.video_types) subliminal-1.1.1/subliminal/providers/tvsubtitles.py0000664000175000017500000001705512640425355024123 0ustar antoineantoine00000000000000# -*- coding: utf-8 -*- import io import logging import re from zipfile import ZipFile from babelfish import Language from requests import Session from . import ParserBeautifulSoup, Provider, get_version from .. import __version__ from ..cache import EPISODE_EXPIRATION_TIME, SHOW_EXPIRATION_TIME, region from ..exceptions import ProviderError from ..subtitle import Subtitle, fix_line_ending, guess_matches, guess_properties, sanitized_string_equal from ..video import Episode logger = logging.getLogger(__name__) link_re = re.compile('^(?P.+?)(?: \(?\d{4}\)?| \((?:US|UK)\))? \((?P\d{4})-\d{4}\)$') episode_id_re = re.compile('^episode-\d+\.html$') class TVsubtitlesSubtitle(Subtitle): provider_name = 'tvsubtitles' def __init__(self, language, page_link, subtitle_id, series, season, episode, year, rip, release): super(TVsubtitlesSubtitle, self).__init__(language, page_link=page_link) self.subtitle_id = subtitle_id self.series = series self.season = season self.episode = episode self.year = year self.rip = rip self.release = release @property def id(self): return str(self.subtitle_id) def get_matches(self, video, hearing_impaired=False): matches = super(TVsubtitlesSubtitle, self).get_matches(video, hearing_impaired=hearing_impaired) # series if video.series and sanitized_string_equal(self.series, video.series): 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') # year if self.year == video.year: matches.add('year') # release_group if video.release_group and self.release and video.release_group.lower() in self.release.lower(): matches.add('release_group') # other properties if self.release: matches |= guess_matches(video, guess_properties(self.release), partial=True) if self.rip: matches |= guess_matches(video, guess_properties(self.rip), partial=True) return matches class TVsubtitlesProvider(Provider): languages = {Language('por', 'BR')} | {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,) server_url = 'http://www.tvsubtitles.net/' def initialize(self): self.session = Session() self.session.headers = {'User-Agent': 'Subliminal/%s' % get_version(__version__)} def terminate(self): self.session.close() @region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME) def search_show_id(self, series, year=None): """Search the show id from the `series` and `year`. :param str series: series of the episode. :param year: year of the series, if any. :type year: int or None :return: the show id, if any. :rtype: int or None """ # make the search logger.info('Searching show id for %r', series) r = self.session.post(self.server_url + 'search.php', data={'q': series}, timeout=10) r.raise_for_status() # get the series out of the suggestions soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser']) show_id = None for suggestion in soup.select('div.left li div a[href^="/tvshow-"]'): match = link_re.match(suggestion.text) if not match: logger.error('Failed to match %s', suggestion.text) continue if match.group('series').lower() == series.lower(): if year is not None and int(match.group('first_year')) != year: logger.debug('Year does not match') continue show_id = int(suggestion['href'][8:-5]) logger.debug('Found show id %d', show_id) break return show_id @region.cache_on_arguments(expiration_time=EPISODE_EXPIRATION_TIME) def get_episode_ids(self, show_id, season): """Get episode ids from the show id and the season. :param int show_id: show id. :param int season: season of the episode. :return: episode ids per episode number. :rtype: dict """ # get the page of the season of the show logger.info('Getting the page of show id %d, season %d', show_id, season) r = self.session.get(self.server_url + 'tvshow-%d-%d.html' % (show_id, season), timeout=10) soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser']) # loop over episode rows episode_ids = {} for row in soup.select('table#table5 tr'): # skip rows that do not have a link to the episode page if not row('a', href=episode_id_re): continue # extract data from the cells cells = row('td') episode = int(cells[0].text.split('x')[1]) episode_id = int(cells[1].a['href'][8:-5]) episode_ids[episode] = episode_id if episode_ids: logger.debug('Found episode ids %r', episode_ids) else: logger.warning('No episode ids found') return episode_ids def query(self, series, season, episode, year=None): # search the show id show_id = self.search_show_id(series, year) if show_id is None: logger.error('No show id found for %r (%r)', series, {'year': year}) return [] # get the episode ids episode_ids = self.get_episode_ids(show_id, season) if episode not in episode_ids: logger.error('Episode %d not found', episode) return [] # get the episode page logger.info('Getting the page for episode %d', episode_ids[episode]) r = self.session.get(self.server_url + 'episode-%d.html' % episode_ids[episode], timeout=10) soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser']) # loop over subtitles rows subtitles = [] for row in soup.select('.subtitlen'): # read the item language = Language.fromtvsubtitles(row.h5.img['src'][13:-4]) subtitle_id = int(row.parent['href'][10:-5]) page_link = self.server_url + 'subtitle-%d.html' % subtitle_id rip = row.find('p', title='rip').text.strip() or None release = row.find('p', title='release').text.strip() or None subtitle = TVsubtitlesSubtitle(language, page_link, subtitle_id, series, season, episode, year, rip, release) logger.debug('Found subtitle %s', subtitle) subtitles.append(subtitle) return subtitles def list_subtitles(self, video, languages): return [s for s in self.query(video.series, video.season, video.episode, video.year) if s.language in languages] def download_subtitle(self, subtitle): # download as a zip logger.info('Downloading subtitle %r', subtitle) r = self.session.get(self.server_url + 'download-%d.html' % subtitle.subtitle_id, timeout=10) r.raise_for_status() # open the zip with ZipFile(io.BytesIO(r.content)) as zf: if len(zf.namelist()) > 1: raise ProviderError('More than one file to unzip') subtitle.content = fix_line_ending(zf.read(zf.namelist()[0])) subliminal-1.1.1/subliminal/providers/podnapisi.py0000664000175000017500000001534712640425355023523 0ustar antoineantoine00000000000000# -*- coding: utf-8 -*- import io import logging import re from babelfish import Language, language_converters from guessit import guess_episode_info, guess_movie_info try: from lxml import etree except ImportError: try: import xml.etree.cElementTree as etree except ImportError: import xml.etree.ElementTree as etree from requests import Session from zipfile import ZipFile from . import Provider, get_version from .. import __version__ from ..exceptions import ProviderError from ..subtitle import Subtitle, fix_line_ending, guess_matches, sanitized_string_equal from ..video import Episode, Movie logger = logging.getLogger(__name__) class PodnapisiSubtitle(Subtitle): provider_name = 'podnapisi' def __init__(self, language, hearing_impaired, page_link, pid, releases, title, season=None, episode=None, year=None): super(PodnapisiSubtitle, self).__init__(language, hearing_impaired, page_link) self.pid = pid self.releases = releases self.title = title self.season = season self.episode = episode self.year = year @property def id(self): return self.pid def get_matches(self, video, hearing_impaired=False): matches = super(PodnapisiSubtitle, self).get_matches(video, hearing_impaired=hearing_impaired) # episode if isinstance(video, Episode): # series if video.series and sanitized_string_equal(self.title, video.series): 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 |= guess_matches(video, guess_episode_info(release + '.mkv')) # movie elif isinstance(video, Movie): # title if video.title and sanitized_string_equal(self.title, video.title): matches.add('title') # guess for release in self.releases: matches |= guess_matches(video, guess_movie_info(release + '.mkv')) # year if video.year and self.year == video.year: matches.add('year') return matches class PodnapisiProvider(Provider): languages = ({Language('por', 'BR'), Language('srp', script='Latn')} | {Language.fromalpha2(l) for l in language_converters['alpha2'].codes}) server_url = 'http://podnapisi.net/subtitles/' def initialize(self): self.session = Session() self.session.headers = {'User-Agent': 'Subliminal/%s' % get_version(__version__)} def terminate(self): self.session.close() def query(self, language, keyword, season=None, episode=None, year=None): # set parameters, see http://www.podnapisi.net/forum/viewtopic.php?f=62&t=26164#p212652 params = {'sXML': 1, 'sL': str(language), 'sK': keyword} is_episode = False if season and episode: is_episode = True params['sTS'] = season params['sTE'] = episode if year: params['sY'] = year # loop over paginated results logger.info('Searching subtitles %r', params) subtitles = [] pids = set() while True: # query the server xml = etree.fromstring(self.session.get(self.server_url + 'search/old', params=params, timeout=10).content) # exit if no results if not int(xml.find('pagination/results').text): logger.debug('No subtitles found') break # loop over subtitles for subtitle_xml in xml.findall('subtitle'): # read xml elements language = Language.fromietf(subtitle_xml.find('language').text) hearing_impaired = 'n' in (subtitle_xml.find('flags').text or '') page_link = subtitle_xml.find('url').text pid = subtitle_xml.find('pid').text releases = [] if subtitle_xml.find('release').text: for release in subtitle_xml.find('release').text.split(): 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) year = int(subtitle_xml.find('year').text) if is_episode: subtitle = PodnapisiSubtitle(language, hearing_impaired, page_link, pid, releases, title, season=season, episode=episode, year=year) else: subtitle = PodnapisiSubtitle(language, hearing_impaired, page_link, pid, releases, title, year=year) # ignore duplicates, see http://www.podnapisi.net/forum/viewtopic.php?f=62&t=26164&start=10#p213321 if pid in pids: continue logger.debug('Found subtitle %r', subtitle) subtitles.append(subtitle) pids.add(pid) # stop on last page if int(xml.find('pagination/current').text) >= int(xml.find('pagination/count').text): break # increment current page params['page'] = int(xml.find('pagination/current').text) + 1 logger.debug('Getting page %d', params['page']) return subtitles def list_subtitles(self, video, languages): if isinstance(video, Episode): return [s for l in languages for s in self.query(l, video.series, season=video.season, episode=video.episode, year=video.year)] elif isinstance(video, Movie): return [s for l in languages for s in self.query(l, video.title, year=video.year)] def download_subtitle(self, subtitle): # download as a zip logger.info('Downloading subtitle %r', subtitle) r = self.session.get(self.server_url + subtitle.pid + '/download', params={'container': 'zip'}, timeout=10) r.raise_for_status() # open the zip with ZipFile(io.BytesIO(r.content)) as zf: if len(zf.namelist()) > 1: raise ProviderError('More than one file to unzip') subtitle.content = fix_line_ending(zf.read(zf.namelist()[0])) subliminal-1.1.1/subliminal/providers/thesubdb.py0000664000175000017500000000505212640425355023325 0ustar antoineantoine00000000000000# -*- coding: utf-8 -*- import logging from babelfish import Language, language_converters from requests import Session from . import Provider, get_version from .. import __version__ from ..subtitle import Subtitle, fix_line_ending logger = logging.getLogger(__name__) class TheSubDBSubtitle(Subtitle): provider_name = 'thesubdb' def __init__(self, language, hash): super(TheSubDBSubtitle, self).__init__(language) self.hash = hash @property def id(self): return self.hash + '-' + str(self.language) def get_matches(self, video, hearing_impaired=False): matches = super(TheSubDBSubtitle, self).get_matches(video, hearing_impaired=hearing_impaired) # hash if 'thesubdb' in video.hashes and video.hashes['thesubdb'] == self.hash: matches.add('hash') return matches class TheSubDBProvider(Provider): languages = {Language.fromthesubdb(l) for l in language_converters['thesubdb'].codes} required_hash = 'thesubdb' server_url = 'http://api.thesubdb.com/' def initialize(self): self.session = Session() self.session.headers = {'User-Agent': 'SubDB/1.0 (subliminal/%s; https://github.com/Diaoul/subliminal)' % get_version(__version__)} def terminate(self): self.session.close() def query(self, hash): # make the query params = {'action': 'search', 'hash': hash} logger.info('Searching subtitles %r', params) r = self.session.get(self.server_url, params=params, timeout=10) # handle subtitles not found and errors if r.status_code == 404: logger.debug('No subtitles found') return [] r.raise_for_status() # loop over languages subtitles = [] for language_code in r.text.split(','): language = Language.fromthesubdb(language_code) subtitle = TheSubDBSubtitle(language, hash) logger.debug('Found subtitle %r', subtitle) subtitles.append(subtitle) return subtitles def list_subtitles(self, video, languages): return [s for s in self.query(video.hashes['thesubdb']) if s.language in languages] def download_subtitle(self, subtitle): logger.info('Downloading subtitle %r', subtitle) params = {'action': 'download', 'hash': subtitle.hash, 'language': subtitle.language.alpha2} r = self.session.get(self.server_url, params=params, timeout=10) r.raise_for_status() subtitle.content = fix_line_ending(r.content) subliminal-1.1.1/subliminal/providers/subscenter.py0000664000175000017500000002163112640606111023672 0ustar antoineantoine00000000000000# -*- coding: utf-8 -*- import bisect import io import json import logging import zipfile from babelfish import Language from guessit import guess_episode_info, guess_movie_info from requests import Session from . import ParserBeautifulSoup, Provider, get_version from .. import __version__ from ..cache import SHOW_EXPIRATION_TIME, region from ..exceptions import AuthenticationError, ConfigurationError, ProviderError from ..subtitle import Subtitle, fix_line_ending, guess_matches, sanitized_string_equal from ..video import Episode, Movie logger = logging.getLogger(__name__) class SubsCenterSubtitle(Subtitle): provider_name = 'subscenter' def __init__(self, language, hearing_impaired, page_link, series, season, episode, title, subtitle_id, subtitle_key, downloaded, releases): super(SubsCenterSubtitle, self).__init__(language, hearing_impaired, page_link) self.series = series self.season = season self.episode = episode self.title = title self.subtitle_id = subtitle_id self.subtitle_key = subtitle_key self.downloaded = downloaded self.releases = releases @property def id(self): return str(self.subtitle_id) def get_matches(self, video, hearing_impaired=False): matches = super(SubsCenterSubtitle, self).get_matches(video, hearing_impaired=hearing_impaired) # episode if isinstance(video, Episode): # series if video.series and sanitized_string_equal(self.series, video.series): 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 |= guess_matches(video, guess_episode_info(release + '.mkv')) # movie elif isinstance(video, Movie): # guess for release in self.releases: matches |= guess_matches(video, guess_movie_info(release + '.mkv')) # title if video.title and sanitized_string_equal(self.title, video.title): matches.add('title') return matches class SubsCenterProvider(Provider): languages = {Language.fromalpha2(l) for l in ['he']} server = 'http://subscenter.cinemast.com/he/' 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 ConfigurationError('Username and password must be specified') self.username = username self.password = password self.logged_in = False def initialize(self): self.session = Session() self.session.headers = {'User-Agent': 'Subliminal/%s' % get_version(__version__)} # login if self.username is not None and self.password is not None: logger.debug('Logging in') url = self.server + 'subscenter/accounts/login/' # retrieve CSRF token self.session.get(url) csrf_token = self.session.cookies['csrftoken'] # actual login data = {'username': self.username, 'password': self.password, 'csrfmiddlewaretoken': csrf_token} r = self.session.post(url, data, allow_redirects=False, timeout=10) if r.status_code != 302: raise AuthenticationError(self.username) logger.info('Logged in') self.logged_in = True def terminate(self): # logout if self.logged_in: logger.info('Logging out') r = self.session.get(self.server + 'subscenter/accounts/logout/', timeout=10) r.raise_for_status() logger.info('Logged out') self.logged_in = False self.session.close() @region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME) def _search_url_title(self, title, kind): """Search the URL title for the given `title`. :param str title: title to search for. :param str kind: kind of the title, ``movie`` or ``series``. :return: the URL version of the title. :rtype: str or None """ # make the search logger.info('Searching title name for %r', title) r = self.session.get(self.server + 'subtitle/search/', params={'q': title}, allow_redirects=False, timeout=10) r.raise_for_status() # if redirected, get the url title from the Location header if r.is_redirect: parts = r.headers['Location'].split('/') # check kind if parts[-3] == kind: return parts[-2] return None # otherwise, get the first valid suggestion soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser']) suggestions = soup.select('#processes div.generalWindowTop a') logger.debug('Found %d suggestions', len(suggestions)) for suggestion in suggestions: parts = suggestion.attrs['href'].split('/') # check kind if parts[-3] == kind: return parts[-2] def query(self, series=None, season=None, episode=None, title=None): # set the correct parameters depending on the kind if series and season and episode: url_series = self._search_url_title(series, 'series') url = self.server + 'cinemast/data/series/sb/{}/{}/{}/'.format(url_series, season, episode) page_link = self.server + 'subtitle/series/{}/{}/{}/'.format(url_series, season, episode) elif title: url_title = self._search_url_title(title, 'movie') url = self.server + 'cinemast/data/movie/sb/{}/'.format(url_title) page_link = self.server + 'subtitle/movie/{}/'.format(url_title) else: raise ValueError('One or more parameters are missing') # get the list of subtitles logger.debug('Getting the list of subtitles') r = self.session.get(url) r.raise_for_status() results = json.loads(r.text) # loop over results subtitles = {} for language_code, language_data in results.items(): for quality_data in language_data.values(): for quality, subtitles_data in quality_data.items(): for subtitle_item in subtitles_data.values(): # read the item language = Language.fromalpha2(language_code) hearing_impaired = bool(subtitle_item['hearing_impaired']) subtitle_id = subtitle_item['id'] subtitle_key = subtitle_item['key'] downloaded = subtitle_item['downloaded'] release = subtitle_item['subtitle_version'] # add the release and increment downloaded count if we already have the subtitle if subtitle_id in subtitles: logger.debug('Found additional release %r for subtitle %d', release, subtitle_id) bisect.insort_left(subtitles[subtitle_id].releases, release) # deterministic order subtitles[subtitle_id].downloaded += downloaded continue # otherwise create it subtitle = SubsCenterSubtitle(language, hearing_impaired, page_link, series, season, episode, title, subtitle_id, subtitle_key, downloaded, [release]) logger.debug('Found subtitle %r', subtitle) subtitles[subtitle_id] = subtitle return subtitles.values() def list_subtitles(self, video, languages): series = None season = None episode = None title = video.title if isinstance(video, Episode): series = video.series season = video.season episode = video.episode return [s for s in self.query(series, season, episode, title) if s.language in languages] def download_subtitle(self, subtitle): # download url = self.server + 'subtitle/download/{}/{}/'.format(subtitle.language.alpha2, subtitle.subtitle_id) params = {'v': subtitle.releases[0], 'key': subtitle.subtitle_key} r = self.session.get(url, params=params, headers={'Referer': subtitle.page_link}, timeout=10) r.raise_for_status() # open the zip with zipfile.ZipFile(io.BytesIO(r.content)) as zf: # remove some filenames from the namelist namelist = [n for n in zf.namelist() if not n.endswith('.txt')] if len(namelist) > 1: raise ProviderError('More than one file to unzip') subtitle.content = fix_line_ending(zf.read(namelist[0])) subliminal-1.1.1/subliminal/providers/addic7ed.py0000664000175000017500000002463612640425355023202 0ustar antoineantoine00000000000000# -*- coding: utf-8 -*- import logging import re from babelfish import Language from requests import Session from . import ParserBeautifulSoup, Provider, get_version from .. import __version__ from ..cache import SHOW_EXPIRATION_TIME, region from ..exceptions import AuthenticationError, ConfigurationError, DownloadLimitExceeded, TooManyRequests from ..subtitle import (Subtitle, fix_line_ending, guess_matches, guess_properties, sanitize_string, sanitized_string_equal) from ..video import Episode logger = logging.getLogger(__name__) series_year_re = re.compile('^(?P[ \w\'.:]+)(?: \((?P\d{4})\))?$') class Addic7edSubtitle(Subtitle): provider_name = 'addic7ed' def __init__(self, language, hearing_impaired, page_link, series, season, episode, title, year, version, download_link): super(Addic7edSubtitle, self).__init__(language, hearing_impaired, page_link) self.series = series self.season = season self.episode = episode self.title = title self.year = year self.version = version self.download_link = download_link @property def id(self): return self.download_link def get_matches(self, video, hearing_impaired=False): matches = super(Addic7edSubtitle, self).get_matches(video, hearing_impaired=hearing_impaired) # series if video.series and sanitized_string_equal(self.series, video.series): 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') # title if video.title and sanitized_string_equal(self.title, video.title): matches.add('title') # year if video.year == self.year: matches.add('year') # release_group if video.release_group and self.version and video.release_group.lower() in self.version.lower(): matches.add('release_group') # resolution if video.resolution and self.version and video.resolution in self.version.lower(): matches.add('resolution') # format if video.format and self.version and video.format.lower() in self.version.lower(): matches.add('format') # other properties matches |= guess_matches(video, guess_properties(self.version), partial=True) return matches class Addic7edProvider(Provider): languages = {Language('por', 'BR')} | {Language(l) for l in [ 'ara', 'aze', 'ben', 'bos', 'bul', 'cat', 'ces', 'dan', 'deu', 'ell', 'eng', 'eus', 'fas', '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,) server_url = '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 ConfigurationError('Username and password must be specified') self.username = username self.password = password self.logged_in = False def initialize(self): self.session = Session() self.session.headers = {'User-Agent': 'Subliminal/%s' % get_version(__version__)} # login if self.username is not None and self.password is not None: logger.info('Logging in') data = {'username': self.username, 'password': self.password, 'Submit': 'Log in'} r = self.session.post(self.server_url + 'dologin.php', data, allow_redirects=False, timeout=10) if r.status_code != 302: raise AuthenticationError(self.username) logger.debug('Logged in') self.logged_in = True def terminate(self): # logout if self.logged_in: logger.info('Logging out') r = self.session.get(self.server_url + 'logout.php', timeout=10) r.raise_for_status() logger.debug('Logged out') self.logged_in = False self.session.close() @region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME) def _get_show_ids(self): """Get the ``dict`` of show ids per series by querying the `shows.php` page. :return: show id per series, lower case and without quotes. :rtype: dict """ # get the show page logger.info('Getting show ids') r = self.session.get(self.server_url + 'shows.php', timeout=10) r.raise_for_status() soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser']) # populate the show ids show_ids = {} for show in soup.select('td.version > h3 > a[href^="/show/"]'): show_ids[sanitize_string(show.text).lower()] = int(show['href'][6:]) logger.debug('Found %d show ids', len(show_ids)) return show_ids @region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME) def _search_show_id(self, series, year=None): """Search the show id from the `series` and `year`. :param str series: series of the episode. :param year: year of the series, if any. :type year: int or None :return: the show id, if found. :rtype: int or None """ # build the params series_year = '%s %d' % (series, year) if year is not None else series params = {'search': sanitize_string(series_year, replacement=' '), 'Submit': 'Search'} # make the search logger.info('Searching show ids with %r', params) r = self.session.get(self.server_url + 'search.php', params=params, timeout=10) r.raise_for_status() soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser']) # get the suggestion suggestion = soup.select('span.titulo > a[href^="/show/"]') if not suggestion: logger.warning('Show id not found: no suggestion') return None if not sanitized_string_equal(suggestion[0].i.text, series_year): logger.warning('Show id not found: suggestion does not match') return None show_id = int(suggestion[0]['href'][6:]) logger.debug('Found show id %d', show_id) return show_id def get_show_id(self, series, year=None, country_code=None): """Get the best matching show id for `series`, `year` and `country_code`. First search in the result of :meth:`_get_show_ids` and fallback on a search with :meth:`_search_show_id` :param str series: series of the episode. :param year: year of the series, if any. :type year: int or None :param country_code: country code of the series, if any. :type country_code: str or None :return: the show id, if found. :rtype: int or None """ series_sanitized = sanitize_string(series).lower() show_ids = self._get_show_ids() show_id = None # attempt with country if not show_id and country_code: logger.debug('Getting show id with country') show_id = show_ids.get('%s %s' % (series_sanitized, country_code.lower())) # attempt with year if not show_id and year: logger.debug('Getting show id with year') show_id = show_ids.get('%s %d' % (series_sanitized, year)) # attempt clean if not show_id: logger.debug('Getting show id') show_id = show_ids.get(series_sanitized) # search as last resort if not show_id: logger.warning('Series not found in show ids') show_id = self._search_show_id(series) return show_id def query(self, series, season, year=None, country=None): # get the show id show_id = self.get_show_id(series, year, country) if show_id is None: logger.error('No show id found for %r (%r)', series, {'year': year, 'country': country}) return [] # get the page of the season of the show logger.info('Getting the page of show id %d, season %d', show_id, season) r = self.session.get(self.server_url + 'show/%d' % show_id, params={'season': season}, timeout=10) r.raise_for_status() if r.status_code == 304: raise TooManyRequests() soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser']) # loop over subtitle rows match = series_year_re.match(soup.select('#header font')[0].text.strip()[:-10]) series = match.group('series') year = int(match.group('year')) if match.group('year') else None subtitles = [] for row in soup.select('tr.epeven'): cells = row('td') # ignore incomplete subtitles status = cells[5].text if status != 'Completed': logger.debug('Ignoring subtitle with status %s', status) continue # read the item language = Language.fromaddic7ed(cells[3].text) hearing_impaired = bool(cells[6].text) page_link = self.server_url + cells[2].a['href'][1:] season = int(cells[0].text) episode = int(cells[1].text) title = cells[2].text version = cells[4].text download_link = cells[9].a['href'][1:] subtitle = Addic7edSubtitle(language, hearing_impaired, page_link, series, season, episode, title, year, version, download_link) logger.debug('Found subtitle %r', subtitle) subtitles.append(subtitle) return subtitles def list_subtitles(self, video, languages): return [s for s in self.query(video.series, video.season, video.year) if s.language in languages and s.episode == video.episode] def download_subtitle(self, subtitle): # download the subtitle logger.info('Downloading subtitle %r', subtitle) r = self.session.get(self.server_url + subtitle.download_link, headers={'Referer': subtitle.page_link}, timeout=10) r.raise_for_status() # detect download limit exceeded if r.headers['Content-Type'] == 'text/html': raise DownloadLimitExceeded subtitle.content = fix_line_ending(r.content) subliminal-1.1.1/subliminal/cli.py0000664000175000017500000004233312640425355020262 0ustar antoineantoine00000000000000# -*- coding: utf-8 -*- """ Subliminal uses `click `_ to provide a powerful :abbr:`CLI (command-line interface)`. """ from __future__ import division from collections import defaultdict from datetime import timedelta import json import logging import os import re 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`.""" 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() 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' def convert(self, value, param, ctx): try: return Language.fromietf(value) except BabelfishError: self.fail('%s is not a valid language' % value) LANGUAGE = LanguageParamType() class AgeParamType(click.ParamType): """:class:`~click.ParamType` for age strings that returns a :class:`~datetime.timedelta` An age string is in the form `number + identifier` with possible identifiers: * ``w`` for weeks * ``d`` for days * ``h`` for hours The form can be specified multiple times but only with that idenfier ordering. For example: * ``1w2d4h`` for 1 week, 2 days and 4 hours * ``2w`` for 2 weeks * ``3w6h`` for 3 weeks and 6 hours """ name = 'age' def convert(self, value, param, ctx): match = re.match(r'^(?:(?P\d+?)w)?(?:(?P\d+?)d)?(?:(?P\d+?)h)?$', value) if not match: self.fail('%s is not a valid age' % value) return timedelta(**{k: int(v) for k, v in match.groupdict(0).items()}) AGE = AgeParamType() PROVIDER = click.Choice(sorted(provider_manager.names())) 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('--opensubtitles', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD', help='OpenSubtitles configuration.') @click.option('--subscenter', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD', help='SubsCenter 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, opensubtitles, subscenter, cache_dir, debug): """Subtitles, faster than your thoughts.""" # create cache directory 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, cache_file), 'lock_factory': MutexLock}) # configure logging if debug: handler = logging.StreamHandler() handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT)) logging.getLogger('subliminal').addHandler(handler) logging.getLogger('subliminal').setLevel(logging.DEBUG) # provider configs 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]} if subscenter: ctx.obj['provider_configs']['subscenter'] = {'username': subscenter[0], 'password': subscenter[1]} @subliminal.command() @click.option('--clear-subliminal', is_flag=True, help='Clear subliminal\'s cache. Use this ONLY if your cache is ' 'corrupted or if you experience issues.') @click.pass_context def cache(ctx, clear_subliminal): """Cache management.""" if clear_subliminal: os.remove(os.path.join(ctx.parent.params['cache_dir'], cache_file)) click.echo('Subliminal\'s cache cleared.') else: click.echo('Nothing done.') @subliminal.command() @click.option('-l', '--language', type=LANGUAGE, required=True, multiple=True, help='Language as IETF code, ' 'e.g. en, pt-BR (can be used multiple times).') @click.option('-p', '--provider', type=PROVIDER, multiple=True, help='Provider to use (can be used multiple times).') @click.option('-a', '--age', type=AGE, help='Filter videos newer than AGE, e.g. 12h, 1w2d.') @click.option('-d', '--directory', type=click.STRING, metavar='DIR', help='Directory where to save subtitles, ' 'default is next to the video file.') @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. 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 (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 def download(obj, provider, language, age, directory, encoding, single, force, hearing_impaired, min_score, verbose, path): """Download best subtitles. PATH can be an directory containing videos, a video file path or a video file name. It can be used multiple times. If an existing subtitle is detected (external or embedded) in the correct language, the download is skipped for the associated video. """ # process parameters language = set(language) # scan videos videos = [] ignored_videos = [] 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): 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): try: scanned_videos = scan_videos(p, subtitles=not force, embedded_subtitles=not force, subtitles_dir=directory) 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: ignored_videos.append(video) continue # other inputs try: video = scan_video(p, subtitles=not force, embedded_subtitles=not force, subtitles_dir=directory) 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' % ( os.path.split(video.name)[1], ', '.join(str(s) for s in video.subtitle_languages) or 'none', video.age.days, 's' if video.age.days > 1 else '' ), fg='yellow') # report collected videos 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: return # download best subtitles downloaded_subtitles = defaultdict(list) with ProviderPool(providers=provider, provider_configs=obj['provider_configs']) as pool: with click.progressbar(videos, label='Downloading subtitles', item_show_func=lambda v: os.path.split(v.name)[1] if v is not None else '') as bar: for v in bar: subtitles = pool.download_best_subtitles(pool.list_subtitles(v, language - v.subtitle_languages), v, language, min_score=v.scores['hash'] * min_score / 100, hearing_impaired=hearing_impaired, only_one=single) downloaded_subtitles[v] = subtitles # save subtitles total_subtitles = 0 for v, subtitles in downloaded_subtitles.items(): saved_subtitles = save_subtitles(v, subtitles, single=single, directory=directory, encoding=encoding) total_subtitles += len(saved_subtitles) if verbose > 0: click.echo('%s subtitle%s downloaded for %s' % (click.style(str(len(saved_subtitles)), bold=True), 's' if len(saved_subtitles) > 1 else '', os.path.split(v.name)[1])) if verbose > 1: for s in saved_subtitles: matches = s.get_matches(v, hearing_impaired=hearing_impaired) score = compute_score(matches, v) # score color score_color = None if isinstance(v, Movie): if score < v.scores['title']: score_color = 'red' elif score < v.scores['title'] + v.scores['year'] + v.scores['release_group']: score_color = 'yellow' else: score_color = 'green' elif isinstance(v, Episode): if score < v.scores['series'] + v.scores['season'] + v.scores['episode']: score_color = 'red' elif score < (v.scores['series'] + v.scores['season'] + v.scores['episode'] + v.scores['release_group']): score_color = 'yellow' else: score_color = 'green' # scale score from 0 to 100 taking out preferences scaled_score = score if s.hearing_impaired == hearing_impaired: scaled_score -= v.scores['hearing_impaired'] scaled_score *= 100 / v.scores['hash'] # echo some nice colored output 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), provider_name=s.provider_name, matches=', '.join(sorted(matches, key=v.scores.get, reverse=True)) )) if verbose == 0: click.echo('Downloaded %s subtitle%s' % (click.style(str(total_subtitles), bold=True), 's' if total_subtitles > 1 else '')) subliminal-1.1.1/subliminal/api.py0000664000175000017500000003770512640425355020273 0ustar antoineantoine00000000000000# -*- coding: utf-8 -*- from collections import defaultdict import io import logging import operator import os.path import socket from babelfish import Language import requests from stevedore import EnabledExtensionManager, ExtensionManager from .subtitle import compute_score, get_subtitle_path logger = logging.getLogger(__name__) provider_manager = ExtensionManager('subliminal.providers') class ProviderPool(object): """A pool of providers with the same API as a single :class:`~subliminal.providers.Provider`. It has a few extra features: * Lazy loads providers when needed and supports the :keyword:`with` statement to :meth:`terminate` the providers on exit. * Automatically discard providers on failure. :param providers: name of providers to use, if not all. :type providers: list :param dict provider_configs: provider configuration as keyword arguments per provider name to pass when instanciating the :class:`~subliminal.providers.Provider`. """ def __init__(self, providers=None, provider_configs=None): #: Name of providers to use self.providers = providers or provider_manager.names() #: Provider configuration self.provider_configs = provider_configs or {} #: Initialized providers self.initialized_providers = {} #: Discarded providers self.discarded_providers = set() #: Dedicated :data:`provider_manager` as :class:`~stevedore.enabled.EnabledExtensionManager` self.manager = EnabledExtensionManager(provider_manager.namespace, lambda e: e.name in self.providers) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.terminate() def __getitem__(self, name): if name not in self.initialized_providers: logger.info('Initializing provider %s', name) provider = self.manager[name].plugin(**self.provider_configs.get(name, {})) provider.initialize() self.initialized_providers[name] = provider return self.initialized_providers[name] def __delitem__(self, name): if name not in self.initialized_providers: raise KeyError(name) try: logger.info('Terminating provider %s', name) self.initialized_providers[name].terminate() except (requests.Timeout, socket.timeout): logger.error('Provider %r timed out, improperly terminated', name) except: logger.exception('Provider %r terminated unexpectedly', name) del self.initialized_providers[name] def __iter__(self): return iter(self.initialized_providers) def list_subtitles(self, video, languages): """List subtitles. :param video: video to list subtitles for. :type video: :class:`~subliminal.video.Video` :param languages: languages to search for. :type languages: set of :class:`~babelfish.language.Language` :return: found subtitles. :rtype: list of :class:`~subliminal.subtitle.Subtitle` """ subtitles = [] for name in self.providers: # check discarded providers if name in self.discarded_providers: logger.debug('Skipping discarded provider %r', name) continue # check video validity if not self.manager[name].plugin.check(video): logger.info('Skipping provider %r: not a valid video', name) continue # check supported languages provider_languages = self.manager[name].plugin.languages & languages if not provider_languages: logger.info('Skipping provider %r: no language to search for', name) continue # list subtitles logger.info('Listing subtitles with provider %r and languages %r', name, provider_languages) try: provider_subtitles = self[name].list_subtitles(video, provider_languages) except (requests.Timeout, socket.timeout): logger.error('Provider %r timed out, discarding it', name) self.discarded_providers.add(name) continue except: logger.exception('Unexpected error in provider %r, discarding it', name) self.discarded_providers.add(name) continue subtitles.extend(provider_subtitles) return subtitles def download_subtitle(self, subtitle): """Download `subtitle`'s :attr:`~subliminal.subtitle.Subtitle.content`. :param subtitle: subtitle to download. :type subtitle: :class:`~subliminal.subtitle.Subtitle` :return: `True` if the subtitle has been successfully downloaded, `False` otherwise. :rtype: bool """ # check discarded providers if subtitle.provider_name in self.discarded_providers: logger.warning('Provider %r is discarded', subtitle.provider_name) return False logger.info('Downloading subtitle %r', subtitle) try: self[subtitle.provider_name].download_subtitle(subtitle) except (requests.Timeout, socket.timeout): logger.error('Provider %r timed out, discarding it', subtitle.provider_name) self.discarded_providers.add(subtitle.provider_name) return False except: logger.exception('Unexpected error in provider %r, discarding it', subtitle.provider_name) self.discarded_providers.add(subtitle.provider_name) return False # check subtitle validity if not subtitle.is_valid(): logger.error('Invalid subtitle') return False return True def download_best_subtitles(self, subtitles, video, languages, min_score=0, hearing_impaired=False, only_one=False, scores=None): """Download the best matching subtitles. :param subtitles: the subtitles to use. :type subtitles: list of :class:`~subliminal.subtitle.Subtitle` :param video: video to download subtitles for. :type video: :class:`~subliminal.video.Video` :param languages: languages to download. :type languages: set of :class:`~babelfish.language.Language` :param int min_score: minimum score for a subtitle to be downloaded. :param bool hearing_impaired: hearing impaired preference. :param bool only_one: download only one subtitle, not one per language. :param dict scores: scores to use, if `None`, the :attr:`~subliminal.video.Video.scores` from the video are used. :return: downloaded subtitles. :rtype: list of :class:`~subliminal.subtitle.Subtitle` """ # sort subtitles by score scored_subtitles = sorted([(s, compute_score(s.get_matches(video, hearing_impaired=hearing_impaired), video, scores=scores)) for s in subtitles], key=operator.itemgetter(1), reverse=True) # download best subtitles, falling back on the next on error downloaded_subtitles = [] for subtitle, score in scored_subtitles: # check score if score < min_score: logger.info('Score %d is below min_score (%d)', score, min_score) break # check downloaded languages if subtitle.language in set(s.language for s in downloaded_subtitles): logger.debug('Skipping subtitle: %r already downloaded', subtitle.language) continue # download logger.info('Downloading subtitle %r with score %d', subtitle, score) if self.download_subtitle(subtitle): downloaded_subtitles.append(subtitle) # stop when all languages are downloaded if set(s.language for s in downloaded_subtitles) == languages: logger.debug('All languages downloaded') break # stop if only one subtitle is requested if only_one: logger.debug('Only one subtitle downloaded') break return downloaded_subtitles def terminate(self): """Terminate all the :attr:`initialized_providers`.""" logger.debug('Terminating initialized providers') for name in list(self.initialized_providers): del self[name] def check_video(video, languages=None, age=None, undefined=False): """Perform some checks on the `video`. All the checks are optional. Return `False` if any of this check fails: * `languages` already exist in `video`'s :attr:`~subliminal.video.Video.subtitle_languages`. * `video` is older than `age`. * `video` has an `undefined` language in :attr:`~subliminal.video.Video.subtitle_languages`. :param video: video to check. :type video: :class:`~subliminal.video.Video` :param languages: desired languages. :type languages: set of :class:`~babelfish.language.Language` :param datetime.timedelta age: maximum age of the video. :param bool undefined: fail on existing undefined language. :return: `True` if the video passes the checks, `False` otherwise. :rtype: bool """ # language test if languages and not (languages - video.subtitle_languages): logger.debug('All languages %r exist', languages) return False # age test if age and video.age > age: logger.debug('Video is older than %r', age) return False # undefined test if undefined and Language('und') in video.subtitle_languages: logger.debug('Undefined language found') return False return True def list_subtitles(videos, languages, **kwargs): """List subtitles. The `videos` must pass the `languages` check of :func:`check_video`. All other parameters are passed onwards to the :class:`ProviderPool` constructor. :param videos: videos to list subtitles for. :type videos: set of :class:`~subliminal.video.Video` :param languages: languages to search for. :type languages: set of :class:`~babelfish.language.Language` :return: found subtitles per video. :rtype: dict of :class:`~subliminal.video.Video` to list of :class:`~subliminal.subtitle.Subtitle` """ listed_subtitles = defaultdict(list) # check videos checked_videos = [] for video in videos: if not check_video(video, languages=languages): logger.info('Skipping video %r', video) continue checked_videos.append(video) # return immediatly if no video passed the checks if not checked_videos: return listed_subtitles # list subtitles with ProviderPool(**kwargs) as pool: for video in checked_videos: logger.info('Listing subtitles for %r', video) subtitles = pool.list_subtitles(video, languages - video.subtitle_languages) listed_subtitles[video].extend(subtitles) logger.info('Found %d subtitle(s)', len(subtitles)) return listed_subtitles def download_subtitles(subtitles, **kwargs): """Download :attr:`~subliminal.subtitle.Subtitle.content` of `subtitles`. All other parameters are passed onwards to the :class:`ProviderPool` constructor. :param subtitles: subtitles to download. :type subtitles: list of :class:`~subliminal.subtitle.Subtitle` """ with ProviderPool(**kwargs) as pool: for subtitle in subtitles: logger.info('Downloading subtitle %r', subtitle) pool.download_subtitle(subtitle) def download_best_subtitles(videos, languages, min_score=0, hearing_impaired=False, only_one=False, scores=None, **kwargs): """List and download the best matching subtitles. The `videos` must pass the `languages` and `undefined` (`only_one`) checks of :func:`check_video`. All other parameters are passed onwards to the :class:`ProviderPool` constructor. :param videos: videos to download subtitles for. :type videos: set of :class:`~subliminal.video.Video` :param languages: languages to download. :type languages: set of :class:`~babelfish.language.Language` :param int min_score: minimum score for a subtitle to be downloaded. :param bool hearing_impaired: hearing impaired preference. :param bool only_one: download only one subtitle, not one per language. :param dict scores: scores to use, if `None`, the :attr:`~subliminal.video.Video.scores` from the video are used. :return: downloaded subtitles per video. :rtype: dict of :class:`~subliminal.video.Video` to list of :class:`~subliminal.subtitle.Subtitle` """ downloaded_subtitles = defaultdict(list) # check videos checked_videos = [] for video in videos: if not check_video(video, languages=languages, undefined=only_one): logger.info('Skipping video %r', video) continue checked_videos.append(video) # return immediatly if no video passed the checks if not checked_videos: return downloaded_subtitles # download best subtitles with ProviderPool(**kwargs) as pool: for video in checked_videos: 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, scores=scores) logger.info('Downloaded %d subtitle(s)', len(subtitles)) downloaded_subtitles[video].extend(subtitles) return downloaded_subtitles def save_subtitles(video, subtitles, single=False, directory=None, encoding=None): """Save subtitles on filesystem. Subtitles are saved in the order of the list. If a subtitle with a language has already been saved, other subtitles with the same language are silently ignored. The extension used is `.lang.srt` by default or `.srt` is `single` is `True`, with `lang` being the IETF code for the :attr:`~subliminal.subtitle.Subtitle.language` of the subtitle. :param video: video of the subtitles. :type video: :class:`~subliminal.video.Video` :param subtitles: subtitles to save. :type subtitles: list of :class:`~subliminal.subtitle.Subtitle` :param bool single: save a single subtitle, default is to save one subtitle per language. :param str directory: path to directory where to save the subtitles, default is next to the video. :param str encoding: encoding in which to save the subtitles, default is to keep original encoding. :return: the saved subtitles :rtype: list of :class:`~subliminal.subtitle.Subtitle` """ saved_subtitles = [] for subtitle in subtitles: # check content if subtitle.content is None: logger.error('Skipping subtitle %r: no content', subtitle) continue # check language if subtitle.language in set(s.language for s in saved_subtitles): 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) if directory is not None: subtitle_path = os.path.join(directory, os.path.split(subtitle_path)[1]) # save content as is or in the specified encoding logger.info('Saving %r to %r', subtitle, subtitle_path) if encoding is None: with io.open(subtitle_path, 'wb') as f: f.write(subtitle.content) else: with io.open(subtitle_path, 'w', encoding=encoding) as f: f.write(subtitle.text) saved_subtitles.append(subtitle) # check single if single: break return saved_subtitles subliminal-1.1.1/README.rst0000664000175000017500000000643712642175051016473 0ustar antoineantoine00000000000000Subliminal ========== Subtitles, faster than your thoughts. .. image:: https://img.shields.io/pypi/v/subliminal.svg :target: https://pypi.python.org/pypi/subliminal :alt: Latest Version .. image:: https://travis-ci.org/Diaoul/subliminal.svg?branch=master :target: https://travis-ci.org/Diaoul/subliminal :alt: Travis CI build status .. image:: https://readthedocs.org/projects/subliminal/badge/?version=latest :target: https://subliminal.readthedocs.org/ :alt: Documentation Status .. image:: https://coveralls.io/repos/Diaoul/subliminal/badge.svg?branch=master&service=github :target: https://coveralls.io/github/Diaoul/subliminal?branch=master :alt: Code coverage .. image:: https://img.shields.io/github/license/Diaoul/subliminal.svg :target: https://github.com/Diaoul/subliminal/blob/master/LICENSE :alt: License .. image:: https://img.shields.io/badge/gitter-join%20chat-1dce73.svg :alt: Join the chat at https://gitter.im/Diaoul/subliminal :target: https://gitter.im/Diaoul/subliminal :Project page: https://github.com/Diaoul/subliminal :Documentation: https://subliminal.readthedocs.org/ Usage ----- CLI ^^^ Download English subtitles:: $ subliminal download -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4 Collecting videos [####################################] 100% 1 video collected / 0 video ignored / 0 error Downloading subtitles [####################################] 100% Downloaded 1 subtitle Library ^^^^^^^ Download best subtitles in French and English for videos less than two weeks old in a video folder: .. code:: python from datetime import timedelta from babelfish import Language from subliminal import download_best_subtitles, region, save_subtitles, scan_videos # configure the cache region.configure('dogpile.cache.dbm', arguments={'filename': 'cachefile.dbm'}) # scan for videos newer than 2 weeks and their existing subtitles in a folder videos = [v for v in scan_videos('/video/folder') if v.age < timedelta(weeks=2)] # download best subtitles subtitles = download_best_subtitles(videos, {Language('eng'), Language('fra')}) # save them to disk, next to the video for v in videos: save_subtitles(v, subtitles[v]) Nautilus integration -------------------- Screenshots ^^^^^^^^^^^ .. image:: http://i.imgur.com/NCwELpB.png :alt: Menu .. image:: http://i.imgur.com/Y58ky88.png :alt: Configuration .. image:: http://i.imgur.com/qem3DGj.png :alt: Choose subtitles Install ^^^^^^^ 1. Install subliminal on your system ``sudo pip install -U subliminal`` 2. Install nautilus-python with your package manager ``sudo apt-get install nautilus-python`` 3. Create the extension directory ``mkdir -p ~/.local/share/nautilus-python/extensions/subliminal`` 4. Copy the script ``cp examples/nautilus.py ~/.local/share/nautilus-python/extensions/subliminal-nautilus.py`` 5. Copy UI files ``cp -R examples/ui ~/.local/share/nautilus-python/extensions/subliminal/`` 6. (Optional) Create a translation directory for your language ``mkdir -p ~/.local/share/nautilus-python/extensions/subliminal/locale/fr/LC_MESSAGES`` 7. (Optional) Install the translation ``msgfmt examples/i18n/fr.po -o ~/.local/share/nautilus-python/extensions/subliminal/locale/fr/LC_MESSAGES/subliminal.mo`` subliminal-1.1.1/setup.cfg0000664000175000017500000000031412642175070016612 0ustar antoineantoine00000000000000[aliases] test = pytest [build_sphinx] source-dir = docs/ build-dir = docs/_build all_files = 1 [upload_sphinx] upload-dir = docs/_build/html [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 subliminal-1.1.1/LICENSE0000664000175000017500000000207112601751321015772 0ustar antoineantoine00000000000000The MIT License (MIT) Copyright (c) 2015 Antoine Bertin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. subliminal-1.1.1/subliminal.egg-info/0000775000175000017500000000000012642175070020624 5ustar antoineantoine00000000000000subliminal-1.1.1/subliminal.egg-info/dependency_links.txt0000664000175000017500000000000112642175067024700 0ustar antoineantoine00000000000000 subliminal-1.1.1/subliminal.egg-info/entry_points.txt0000664000175000017500000000125512642175067024133 0ustar antoineantoine00000000000000[babelfish.language_converters] addic7ed = subliminal.converters.addic7ed:Addic7edConverter thesubdb = subliminal.converters.thesubdb:TheSubDBConverter tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter [console_scripts] subliminal = subliminal.cli:subliminal [subliminal.providers] addic7ed = subliminal.providers.addic7ed:Addic7edProvider opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider podnapisi = subliminal.providers.podnapisi:PodnapisiProvider subscenter = subliminal.providers.subscenter:SubsCenterProvider thesubdb = subliminal.providers.thesubdb:TheSubDBProvider tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider subliminal-1.1.1/subliminal.egg-info/SOURCES.txt0000664000175000017500000000154412642175070022514 0ustar antoineantoine00000000000000HISTORY.rst LICENSE MANIFEST.in README.rst requirements.txt setup.cfg setup.py subliminal/__init__.py subliminal/api.py subliminal/cache.py subliminal/cli.py subliminal/exceptions.py subliminal/score.py subliminal/subtitle.py subliminal/video.py subliminal.egg-info/PKG-INFO subliminal.egg-info/SOURCES.txt subliminal.egg-info/dependency_links.txt subliminal.egg-info/entry_points.txt subliminal.egg-info/requires.txt subliminal.egg-info/top_level.txt subliminal/converters/__init__.py subliminal/converters/addic7ed.py subliminal/converters/thesubdb.py subliminal/converters/tvsubtitles.py subliminal/providers/__init__.py subliminal/providers/addic7ed.py subliminal/providers/napiprojekt.py subliminal/providers/opensubtitles.py subliminal/providers/podnapisi.py subliminal/providers/subscenter.py subliminal/providers/thesubdb.py subliminal/providers/tvsubtitles.pysubliminal-1.1.1/subliminal.egg-info/PKG-INFO0000664000175000017500000002661412642175067021740 0ustar antoineantoine00000000000000Metadata-Version: 1.1 Name: subliminal Version: 1.1.1 Summary: Subtitles, faster than your thoughts Home-page: https://github.com/Diaoul/subliminal Author: Antoine Bertin Author-email: diaoulael@gmail.com License: MIT Description: Subliminal ========== Subtitles, faster than your thoughts. .. image:: https://img.shields.io/pypi/v/subliminal.svg :target: https://pypi.python.org/pypi/subliminal :alt: Latest Version .. image:: https://travis-ci.org/Diaoul/subliminal.svg?branch=master :target: https://travis-ci.org/Diaoul/subliminal :alt: Travis CI build status .. image:: https://readthedocs.org/projects/subliminal/badge/?version=latest :target: https://subliminal.readthedocs.org/ :alt: Documentation Status .. image:: https://coveralls.io/repos/Diaoul/subliminal/badge.svg?branch=master&service=github :target: https://coveralls.io/github/Diaoul/subliminal?branch=master :alt: Code coverage .. image:: https://img.shields.io/github/license/Diaoul/subliminal.svg :target: https://github.com/Diaoul/subliminal/blob/master/LICENSE :alt: License .. image:: https://img.shields.io/badge/gitter-join%20chat-1dce73.svg :alt: Join the chat at https://gitter.im/Diaoul/subliminal :target: https://gitter.im/Diaoul/subliminal :Project page: https://github.com/Diaoul/subliminal :Documentation: https://subliminal.readthedocs.org/ Usage ----- CLI ^^^ Download English subtitles:: $ subliminal download -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4 Collecting videos [####################################] 100% 1 video collected / 0 video ignored / 0 error Downloading subtitles [####################################] 100% Downloaded 1 subtitle Library ^^^^^^^ Download best subtitles in French and English for videos less than two weeks old in a video folder: .. code:: python from datetime import timedelta from babelfish import Language from subliminal import download_best_subtitles, region, save_subtitles, scan_videos # configure the cache region.configure('dogpile.cache.dbm', arguments={'filename': 'cachefile.dbm'}) # scan for videos newer than 2 weeks and their existing subtitles in a folder videos = [v for v in scan_videos('/video/folder') if v.age < timedelta(weeks=2)] # download best subtitles subtitles = download_best_subtitles(videos, {Language('eng'), Language('fra')}) # save them to disk, next to the video for v in videos: save_subtitles(v, subtitles[v]) Nautilus integration -------------------- Screenshots ^^^^^^^^^^^ .. image:: http://i.imgur.com/NCwELpB.png :alt: Menu .. image:: http://i.imgur.com/Y58ky88.png :alt: Configuration .. image:: http://i.imgur.com/qem3DGj.png :alt: Choose subtitles Install ^^^^^^^ 1. Install subliminal on your system ``sudo pip install -U subliminal`` 2. Install nautilus-python with your package manager ``sudo apt-get install nautilus-python`` 3. Create the extension directory ``mkdir -p ~/.local/share/nautilus-python/extensions/subliminal`` 4. Copy the script ``cp examples/nautilus.py ~/.local/share/nautilus-python/extensions/subliminal-nautilus.py`` 5. Copy UI files ``cp -R examples/ui ~/.local/share/nautilus-python/extensions/subliminal/`` 6. (Optional) Create a translation directory for your language ``mkdir -p ~/.local/share/nautilus-python/extensions/subliminal/locale/fr/LC_MESSAGES`` 7. (Optional) Install the translation ``msgfmt examples/i18n/fr.po -o ~/.local/share/nautilus-python/extensions/subliminal/locale/fr/LC_MESSAGES/subliminal.mo`` Changelog --------- 1.1.1 ^^^^^ **release date:** 2016-01-03 * Fix scanning videos on bad MKV files 1.1 ^^^ **release date:** 2015-12-29 * Fix library usage example in README * Fix for series name with special characters in addic7ed provider * Fix id property in thesubdb provider * Improve matching on titles * Add support for nautilus context menu with translations * Add support for searching subtitles in a separate directory * Add subscenter provider * Add support for python 3.5 1.0.1 ^^^^^ **release date:** 2015-07-23 * Fix unicode issues in CLI (python 2 only) * Fix score scaling in CLI (python 2 only) * Improve error handling in CLI * Color collect report in CLI 1.0 ^^^ **release date:** 2015-07-22 * Many changes and fixes * New test suite * New documentation * New CLI * Added support for SubsCenter 0.7.5 ^^^^^ **release date:** 2015-03-04 * Update requirements * Remove BierDopje provider * Add pre-guessed video optional argument in scan_video * Improve hearing impaired support * Fix TVSubtitles and Podnapisi providers 0.7.4 ^^^^^ **release date:** 2014-01-27 * Fix requirements for guessit and babelfish 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 **WARNING:** Complete rewrite of subliminal with backward incompatible changes * Use enzyme to parse metadata of videos * Use babelfish to handle languages * Use dogpile.cache for caching * Use charade to detect subtitle encoding * Use pysrt for subtitle validation * Use entry points for subtitle providers * New subtitle score computation * Hearing impaired subtitles support * Drop async support * 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 * Fix BierDopje * Fix Addic7ed * Fix SubsWiki * Fix missing enzyme import * Add Catalan and Galician languages to Addic7ed * Add possible services in help message of the CLI * Allow existing filenames to be passed without the ./ prefix 0.6.1 ^^^^^ **release date:** 2012-06-24 * Fix subtitle release name in BierDopje * Fix subtitles being downloaded multiple times * Add Chinese support to TvSubtitles * Fix encoding issues * Fix single download subtitles without the force option * Add Spanish (Latin America) exception to Addic7ed * Fix group_by_video when a list entry has None as subtitles * Add support for Galician language in Subtitulos * Add an integrity check after subtitles download for Addic7ed * Add error handling for if not strict in Language * Fix TheSubDB hash method to return None if the file is too small * Fix guessit.Language in Video.scan * Fix language detection of subtitles 0.6.0 ^^^^^ **release date:** 2012-06-16 **WARNING:** Backward incompatible changes * Fix --workers option in CLI * Use a dedicated module for languages * Use beautifulsoup4 * Improve return types * Add scan_filter option * Add --age option in CLI * Add TvSubtitles service * Add Addic7ed service 0.5.1 ^^^^^ **release date:** 2012-03-25 * Improve error handling of enzyme parsing 0.5 ^^^ **release date:** 2012-03-25 **WARNING:** Backward incompatible changes * Use more unicode * New list_subtitles and download_subtitles methods * New Pool object for asynchronous work * Improve sort algorithm * Better error handling * Make sorting customizable * Remove class Subliminal * Remove permissions handling 0.4 ^^^ **release date:** 2011-11-11 * Many fixes * Better error handling 0.3 ^^^ **release date:** 2011-08-18 * Fix a bug when series is not guessed by guessit * Fix dependencies failure when installing package * Fix encoding issues with logging * Add a script to ease subtitles download * Add possibility to choose mode of created files * Add more checks before adjusting permissions 0.2 ^^^ **release date:** 2011-07-11 * Fix plugin configuration * Fix some encoding issues * Remove extra logging 0.1 ^^^ **release date:** *private release* * Initial release Keywords: subtitle subtitles video movie episode tv show Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Multimedia :: Video subliminal-1.1.1/subliminal.egg-info/top_level.txt0000664000175000017500000000001312642175067023356 0ustar antoineantoine00000000000000subliminal subliminal-1.1.1/subliminal.egg-info/requires.txt0000664000175000017500000000044412642175067023234 0ustar antoineantoine00000000000000guessit>=0.9.1,<2.0 babelfish>=0.5.2 enzyme>=0.4.1 beautifulsoup4>=4.2.0 requests>=2.0 click>=4.0 dogpile.cache>=0.5.4 stevedore>=1.0.0 chardet>=2.3.0 pysrt>=1.0.1 six>=1.9.0 [dev] tox sphinx transifex-client wheel [test] sympy vcrpy>=1.6.1 pytest pytest-pep8 pytest-flakes pytest-cov mock subliminal-1.1.1/MANIFEST.in0000664000175000017500000000005512601751321016523 0ustar antoineantoine00000000000000include LICENSE HISTORY.rst requirements.txt