Compare commits

..

274 Commits

Author SHA1 Message Date
Antoine Bertin c1fda7f44c Release 0.7.1 2013-11-06 00:32:33 +01:00
Antoine Bertin ce42201eee Improve exceptions in addic7ed terminate 2013-11-06 00:31:49 +01:00
Antoine Bertin f65131e5b0 Use the full path to guess in scan_video 2013-11-06 00:20:26 +01:00
Antoine Bertin 8d9efa5dc0 Catch exceptions during provider terminate in api 2013-11-06 00:19:45 +01:00
Antoine Bertin 1a54bfb732 Fix ProviderNotAvailable in list_subtitles 2013-11-06 00:03:53 +01:00
Antoine Bertin a15c1f05b2 Skip links and hidden files and folders in scan_video 2013-11-05 23:25:57 +01:00
Antoine Bertin ccfd341fe9 Fix enzyme track detection 2013-11-05 00:23:24 +01:00
Antoine Bertin aab8e0aa4d Fix spacing around operator 2013-11-04 23:14:25 +01:00
Antoine Bertin 9b669c8a3d Update opensubtitles unittests 2013-11-04 00:56:21 +01:00
Antoine Bertin 50960fed24 Fix single subtitles being always downloaded 2013-11-04 00:43:16 +01:00
Antoine Bertin ff61bc8d2d Add colors to debug log 2013-11-04 00:34:02 +01:00
Antoine Bertin e5d9c229ed Improve CLI 2013-11-04 00:08:20 +01:00
Antoine Bertin b0e38c7e2c Remove unnecessary personal information from addic7ed logging 2013-11-04 00:08:10 +01:00
Antoine Bertin c12dade5ea Add tests for scan_video 2013-11-03 11:53:29 -05:00
Antoine Bertin cb35dabf31 Remove lxml dependency 2013-11-03 11:52:39 -05:00
Antoine Bertin a66cf4b501 Require babelfish 0.2.1 2013-11-03 11:29:42 -05:00
Antoine Bertin c355d6a24a Fix hashes computation for small files 2013-11-03 11:26:34 -05:00
Antoine Bertin 60c1e93037 Scan for und languages and fix download for single 2013-11-01 08:38:24 +01:00
Antoine Bertin 708126aca3 Fix subtitle language filtering of videos 2013-10-31 23:24:49 +01:00
Antoine Bertin bf763a3ad7 Always log found embedded subtitles 2013-10-31 23:23:36 +01:00
Antoine Bertin bb0c3b91a2 Fix the download loop not breaking when done 2013-10-31 22:49:46 +01:00
Antoine Bertin a7c0cd0d19 Fix intersection of sets 2013-10-31 22:49:01 +01:00
Antoine Bertin f315ef9bd0 Fix subtitle language detection in api 2013-10-31 22:32:43 +01:00
Antoine Bertin b262a5491c Fix embedded subtitles always being scanned 2013-10-31 20:47:28 +01:00
Antoine Bertin d7b30336b6 Fix badly encoded subtitles 2013-10-31 20:46:19 +01:00
Antoine Bertin c834bac460 Use absolute paths in cli 2013-10-31 08:37:37 +01:00
Antoine Bertin 36da0a1204 Validate cache-file in cli 2013-10-31 08:37:28 +01:00
Antoine Bertin 6ece9271eb Require absolute paths in scan_video and scan_videos 2013-10-31 08:36:13 +01:00
Antoine Bertin a465058bb3 Catch enzyme and babelfish exceptions in scan_video 2013-10-30 22:47:13 +01:00
Antoine Bertin bad7dbb55c Explicitly use utf-8 for subtitle files encoding 2013-10-30 21:07:18 +01:00
Antoine Bertin 501aaf076e Handle download limit in Addic7ed 2013-10-30 21:05:48 +01:00
Antoine Bertin 49d27cc7e4 Add login support for Addic7ed 2013-10-30 08:40:03 +01:00
Antoine Bertin e3903f77e9 Use babelfish 0.2.0 2013-10-30 08:39:06 +01:00
Antoine Bertin b6ee9b5d7e Fix required language argument in cli 2013-10-30 08:30:11 +01:00
Antoine Bertin e3301cefd7 Fix language extensions loading
The alpha2 converter might not yet be loaded at this point.
When subliminal's converters are loaded, they import subliminal
which import the video module.
2013-10-30 08:29:43 +01:00
Antoine Bertin 74ac38329e Fix None not being a valid language for embedded subtitles 2013-10-30 08:24:15 +01:00
Antoine Bertin 50c39382e6 Improve error logging 2013-10-29 18:07:26 +01:00
Antoine Bertin 9a65708855 Add missing age implementation 2013-10-29 18:06:57 +01:00
Antoine Bertin 4ebcb2cc95 Fix video_types in some providers 2013-10-29 17:26:56 +01:00
Antoine Bertin 0c3c41fb4a Fix unsupported type for timedelta in cli 2013-10-29 17:26:29 +01:00
Antoine Bertin f44942f78e Switch to 0.7.1 2013-10-29 17:23:41 +01:00
Antoine Bertin d7f2211800 Update dev requirements for sphinx documentation upload in pypi 2013-10-29 12:59:50 +01:00
Antoine Bertin f11402c452 Complete rewrite of subliminal 2013-10-29 12:22:21 +01:00
Antoine Bertin 7878fb2f92 Update unittests 2013-01-20 00:01:52 +01:00
Antoine Bertin f90c4634c2 Revamp TVsubtitles
- Use dogpile.cache
- Compute confidence based on votes
- Improved keywords detection
- Add docstrings
- Rename to TVsubtitles
- Add unittests
2013-01-19 23:26:40 +01:00
Antoine Bertin b52fbc8af5 Unset language_code in PodnapisiWeb 2013-01-19 21:42:34 +01:00
Antoine Bertin cde1175a7b Update default User-Agent 2013-01-19 21:42:06 +01:00
Antoine Bertin 30786743e3 Update TheSubDB and add unittests 2013-01-19 15:25:45 +01:00
Antoine Bertin c675bad95c Update Subtitulos and add unittests 2013-01-19 15:03:35 +01:00
Antoine Bertin 288b92d2ce Update SubsWiki and add unittests 2013-01-19 15:03:06 +01:00
Antoine Bertin f6e656117c Add unittests for PodnapisiWeb 2013-01-19 15:02:11 +01:00
Antoine Bertin 896baa6a04 Add unittests for Podnapisi 2013-01-18 20:54:37 +01:00
Antoine Bertin b8f820a9af Fix download method in Podnapisi 2013-01-18 20:52:52 +01:00
Antoine Bertin eb8b046dbd Use new is_valid_subtitle method
Thanks to byroot
2013-01-18 20:52:13 +01:00
Antoine Bertin ff9e0f3190 Fix Video path being empty even if forced in unittests 2013-01-18 20:51:27 +01:00
Antoine Bertin e4f10b9e77 Update addic7ed to use dogpile.cache 2013-01-13 21:25:13 +01:00
Antoine Bertin 5ca2623c13 Merge remote-tracking branch 'rik/requests-changes-172' into develop 2013-01-13 16:47:59 +01:00
Antoine Bertin e560823271 Update setup 2013-01-13 16:46:27 +01:00
Antoine Bertin 3f83b13a29 Use setuptools entry points 2013-01-13 16:46:20 +01:00
Antoine Bertin 507bf09466 Update version to 0.7-dev 2013-01-13 15:57:34 +01:00
Antoine Bertin 688a07dbe9 Update requirements 2013-01-13 15:55:36 +01:00
Antoine Bertin e7c4bc9fc3 Move tests under subliminal module 2013-01-13 15:53:24 +01:00
Anthony Ricaud 04c74afad6 Update to the latest requests API.
fix #172
2013-01-13 14:30:33 +01:00
Antoine Bertin e8a4521b7e Add query unittests for BierDopje and OpenSubtitles 2012-12-16 21:31:49 +01:00
Antoine Bertin 768b9d203c Update new unittests
- Do not expect the exact count of results in list tests
- Download tests
2012-12-16 10:33:32 +01:00
Antoine Bertin ba5e03342f Use key_mangler to fix the error in dbm backend in dogpile.cache 2012-12-15 13:33:25 +01:00
Antoine Bertin cb3ff0334d Add unittests for BierDopje 2012-12-15 13:32:38 +01:00
Antoine Bertin b96bf9b9a5 Finish list unittests for OpenSubtitles 2012-12-15 13:08:05 +01:00
Antoine Bertin 4bf673dfc6 Update new unittests 2012-12-11 14:55:01 +01:00
Antoine Bertin 223fe8025c Use dogpile.cache 2012-12-10 23:31:44 +01:00
Antoine Bertin cf47cf1512 Update requirements
- Add charade and pysrt as test_require
- Add dogpile.cache as install_require
2012-12-10 23:27:57 +01:00
Antoine Bertin 1bac0c3e8c New unittests for services using a yaml configuration file 2012-12-10 23:10:15 +01:00
Antoine Bertin 4634c353d8 Fix subswiki on some movies 2012-12-10 14:05:33 +01:00
Antoine Bertin 9ac2dde4e1 Shorten travis install part 2012-12-09 16:22:58 +01:00
Antoine Bertin 07a6db43e6 Fix test_videos by using unicode 2012-12-09 13:34:17 +01:00
Antoine Bertin c40f1f0cc2 Update unittests for PodnapisiWeb 2012-12-09 11:12:06 +01:00
Antoine Bertin c4ddc6d793 Fix subtitles validation
Subtitles are considered valid if their first 50 lines are
2012-12-09 11:11:48 +01:00
Antoine Bertin 8f58974a1a Remove keywords parameter in PodnapisiWeb as it is not supported 2012-12-09 10:48:07 +01:00
Antoine Bertin b17fbd6fc0 Ensure paths are unicode in get_defaults 2012-12-09 10:46:47 +01:00
Antoine Bertin 57d5d57c53 Fix subtitles validation 2012-12-09 01:14:13 +01:00
Antoine Bertin 5436512b1f Allow empty keywords in services
Applies to addic7ed, subswiki and subtitulos
2012-12-09 00:42:32 +01:00
Antoine Bertin 40e10ea72c Replace filesizes checks in unittests with subtitles validation
Use pysrt for subtitles validation
2012-12-09 00:38:34 +01:00
Antoine Bertin ea6aba4e92 Skip badly encoded paths and require unicode for api calls 2012-12-08 20:21:45 +01:00
Antoine Bertin e4817dd0d3 Update filesizes in unittests 2012-12-07 14:55:55 +01:00
Antoine Bertin 751a2d822a Fix PodnapisiWeb service
- Remove the sR parameter that does not accept multiple
  values and is case-sensitive
- Ensure the languageId is an integer
- Update filesizes in unittests
2012-12-07 14:55:43 +01:00
Antoine Bertin 38cc8422b6 Fix PodnapisiWeb and add unittests 2012-09-23 15:03:06 +02:00
Antoine Bertin 35f2e47195 Fix unittests for SubsWiki 2012-09-23 14:53:20 +02:00
Antoine Bertin ab3ad0fe70 Merge pull request #113 from abenea/podnapisi
Podnapisi
2012-09-22 06:37:13 -07:00
Antoine Bertin ccf26730a2 Fix repr of ServiceConfig when no cache_dir is given 2012-09-22 13:17:36 +02:00
Antoine Bertin c23868dfb1 Merge remote-tracking branch 'wackou/develop' into develop
- Move get_defaults to core
- Add new functions to __all__
- Fix download_subtitles in api
2012-09-22 12:43:03 +02:00
Antoine Bertin b24af17326 Add python 2.6 compatibility on download_zip_file 2012-09-22 11:53:37 +02:00
Antoine Bertin 45aff11bff Fix Subtitulos on incomplete subtitles and use bs4 syntax 2012-09-22 11:45:36 +02:00
Antoine Bertin 1ee700fa9d Merge branch 'develop' 2012-09-15 13:29:27 +02:00
Antoine Bertin 4f74dc9031 Bump version number 2012-09-15 13:28:11 +02:00
Antoine Bertin b53fd0bd61 Fix enzyme import in videos 2012-09-15 13:05:58 +02:00
Antoine Bertin 80e3514d56 Update copyright notice on Addic7ed 2012-09-15 13:05:35 +02:00
Antoine Bertin 3f1cac3ccc Add Galician and Catalan languages to Addic7ed 2012-09-15 13:05:21 +02:00
Antoine Bertin 261e4e8f67 Fix OpenSubtitles testcase 2012-09-15 11:58:20 +02:00
Antoine Bertin bec7ec1901 Fix SubsWiki 2012-09-15 11:29:45 +02:00
Antoine Bertin 69015293a4 Fix OpenSubtitles testcase 2012-09-15 11:28:58 +02:00
Antoine Bertin ca55e417ee Remove unused function in Addic7ed 2012-09-15 11:28:40 +02:00
Antoine Bertin ed37415ee2 Use relative imports in Subtitulos 2012-09-12 23:59:31 +02:00
Antoine Bertin 68dc99f7ab Fix Addic7ed 2012-09-12 23:51:17 +02:00
Antoine Bertin 4d9cac8941 Fix unittests for BierDopje 2012-09-12 21:52:59 +02:00
Antoine Bertin fbd6fe00d6 Add a user agent to BierDopje as requested by the service 2012-09-12 21:52:40 +02:00
Antoine Bertin e491680dff List supported services in CLI help message 2012-09-12 21:29:11 +02:00
Antoine Bertin ffc8474918 Test current directory if no folder is given while scanning 2012-09-12 21:28:43 +02:00
Antoine Bertin dd7f26e51e Update diaoul-sphinx-themes 2012-09-12 07:42:13 +02:00
Nicolas Wack 498460031e Fixed missing import 2012-07-30 00:25:57 +02:00
Nicolas Wack d977e3569f Fixed get_defaults() function call 2012-07-30 00:25:05 +02:00
Antoine Bertin f122e7e4ed Merge pull request #114 from abenea/ass
Add the .ass subtitle extension
2012-07-15 02:13:40 -07:00
Andrei Benea 3af3db3df6 Request XML search results. 2012-07-09 21:35:46 +03:00
Andrei Benea f4246de8a7 Add the .ass subtitle extension. 2012-07-07 11:20:54 +03:00
Andrei Benea 43b647ed28 Enable the PodnapisiWeb service. 2012-07-07 11:09:18 +03:00
Andrei Benea 04958f63de PodnapisiWeb service based on the simple web interface. 2012-07-07 05:03:43 +03:00
Nicolas Wack b15b2c0e7b Factored out consume_task_list() to avoid code duplication 2012-07-01 16:39:45 +02:00
Nicolas Wack 61909b68cc Factored out get_defaults() function to avoid code duplication 2012-07-01 16:37:03 +02:00
Antoine Bertin 71c91bed29 Control subtitles naming in unittest 2012-06-26 19:49:34 +02:00
Antoine Bertin a8aa57dcd6 Merge branch 'develop' 2012-06-24 22:56:03 +02:00
Antoine Bertin a7de8c81b4 Bump version 2012-06-24 22:55:47 +02:00
Antoine Bertin c2688fe81c Remove references to Podnapisi as it is not ready yet 2012-06-24 22:54:42 +02:00
Antoine Bertin 295474506b Update NEWS 2012-06-24 22:49:24 +02:00
Antoine Bertin c4a989dd3d Add the release name in the repr of ResultSubtitle if available 2012-06-24 21:48:18 +02:00
Antoine Bertin 7f6e192149 Add more logging in matching_confidence 2012-06-24 21:46:54 +02:00
Antoine Bertin 28b40e9174 Fix subtitle release name in BierDopje
Matching confidence could not be computed because of the
missing extension
2012-06-24 21:46:33 +02:00
Antoine Bertin f2d2da94a1 Add the path to the repr of a ResultSubtitle 2012-06-24 15:43:44 +02:00
Antoine Bertin 14d19ff090 Fix subtitles being downloaded multiple times
This happened with the multi option because subtitles were
grouped by languages and Language('en-US') is different from
Language('en'). Now we take into account user's languages
preferred order
2012-06-24 15:43:09 +02:00
Antoine Bertin a547464d1e Add Chineese exception to TvSubtitles 2012-06-24 14:49:46 +02:00
Antoine Bertin 23b9aba560 Fix unicode representation of Video when it does not exist 2012-06-24 14:20:10 +02:00
Antoine Bertin 347038c528 Add test_videos to the main test suite 2012-06-24 13:53:41 +02:00
Antoine Bertin 2fc26d910a Use positional arguments for required fields of ResultSubtitle 2012-06-24 13:53:03 +02:00
Antoine Bertin 4828730ea3 Fix some encoding issues 2012-06-24 13:52:28 +02:00
Antoine Bertin 7c4f539a44 Use None as default for keywords in ResultSubtitle constructor 2012-06-24 13:49:13 +02:00
Antoine Bertin 598ef91a30 Do not convert to absolute paths in scan 2012-06-24 13:47:40 +02:00
Antoine Bertin 4862f12619 Update NEWS 2012-06-23 12:35:26 +02:00
Antoine Bertin c96ac214bb Update unittests 2012-06-23 11:24:00 +02:00
Antoine Bertin 8fb9cf6a0b Add __repr__ to Subtitles 2012-06-23 11:22:56 +02:00
Antoine Bertin 4a177b6008 Fix single download subtitles without the force option 2012-06-23 00:23:50 +02:00
Antoine Bertin 58b59a3304 Improve the download_zip_file method 2012-06-20 21:42:51 +02:00
Antoine Bertin 83e84a24b1 Always return the subtitle in Service.download 2012-06-20 21:18:54 +02:00
Antoine Bertin 322e6c1f1c Add Spanish (Latin America) exception to Addic7ed 2012-06-20 21:17:47 +02:00
Antoine Bertin d1ca77d7db Improve Addic7ed subtitles validation 2012-06-20 08:19:19 +02:00
Antoine Bertin d885c78b9a Fix group_by_video when a list entry has None as subtitles 2012-06-20 08:18:37 +02:00
Antoine Bertin 6c8a8a53e7 Avoid some other Addic7ed errors 2012-06-19 23:24:16 +02:00
Antoine Bertin 21ec9335fc Add support for Galician language in Subtitulos 2012-06-19 08:12:31 +02:00
Antoine Bertin 4c40a463da Add an integrity check after subtitles download for Addic7ed 2012-06-19 08:11:54 +02:00
Antoine Bertin 169e97975d Improve logging for file downloads 2012-06-19 08:11:06 +02:00
Antoine Bertin e26c65d4f1 Add error handling for if not strict in Language 2012-06-19 08:10:25 +02:00
Antoine Bertin d1dd86c825 Add possible filesizes for OpenSubtitles in unittests 2012-06-17 19:50:29 +02:00
Antoine Bertin e8388a757b Fix TheSubDB hash method to return None if the file is too small 2012-06-17 18:17:11 +02:00
Antoine Bertin 51c7d46390 Update services unittests
- Remove useless import
- Do not set verbosity
2012-06-17 12:26:16 +02:00
Antoine Bertin 84688acf32 Replace guessit.Language in Video.scan 2012-06-17 11:30:16 +02:00
Antoine Bertin f16ecd220a Fix language detection of subtitles 2012-06-17 11:28:32 +02:00
Antoine Bertin a0f89e46a8 Remove extra skip in unittests 2012-06-17 11:26:07 +02:00
Antoine Bertin 6fce503814 Merge branch 'develop' 2012-06-16 08:07:20 +02:00
Antoine Bertin e873a2fbd2 Put release date in NEWS 2012-06-16 08:06:03 +02:00
Antoine Bertin 53e15969c1 Merge branch 'develop' 2012-06-16 08:02:57 +02:00
Antoine Bertin c11d83a204 Update README 2012-06-16 00:28:05 +02:00
Antoine Bertin 35d664d414 Update documentation 2012-06-16 00:25:55 +02:00
Antoine Bertin 11a33e5425 Use travis-ci sidebar in the documentation 2012-06-16 00:25:43 +02:00
Antoine Bertin 97bd82967e Update diaoul-sphinx-themes 2012-06-16 00:24:30 +02:00
Antoine Bertin ca89339042 Fix CLI 2012-06-15 23:54:25 +02:00
Antoine Bertin 07054c3f68 Fix some naming mistakes 2012-06-15 22:56:11 +02:00
Antoine Bertin 72405f5c0e Add notifications and fix installation in travis-ci 2012-06-15 22:44:41 +02:00
Antoine Bertin d5232b8a68 Add the build status in README 2012-06-15 22:43:39 +02:00
Antoine Bertin d44d032e93 Update requirements
- Add EOL in requirements.txt
- Add optional-requirements.txt for lxml
2012-06-15 22:42:32 +02:00
Antoine Bertin d7122a9c98 Clean up
- Remove blank lines
- Move the strict test in Language
- Update comments in thesubdb
- Order language_map with codes first
2012-06-15 22:40:56 +02:00
Antoine Bertin eb0b9f29f2 Avoid ValueError when a new code is found 2012-06-15 22:38:44 +02:00
Antoine Bertin a8b7763a13 Call parent __init__ in Tasks 2012-06-15 22:37:51 +02:00
Antoine Bertin 1e7fe9d216 Fix encoding issues 2012-06-15 22:37:20 +02:00
Antoine Bertin a0dbfe8c4b Refactor unittests 2012-06-15 21:00:49 +02:00
Antoine Bertin ef2571d626 Fix return type of consume_task for DownloadTasks 2012-06-15 21:00:03 +02:00
Antoine Bertin d3cde7bd05 Update travis-ci file 2012-06-15 18:40:13 +02:00
Antoine Bertin 01191d632b Update NEWS 2012-06-15 18:32:25 +02:00
Antoine Bertin 5c934766f5 Clean up
- Improve imports
- Remove unused exceptions
- Remove blank lines
- Add __all__ in modules
2012-06-15 18:31:56 +02:00
Antoine Bertin decd0e2510 Use the same return type between list_sutbitles and download_subtitles 2012-06-15 17:54:22 +02:00
Antoine Bertin cd46dad14b Add unittests for api 2012-06-15 17:47:15 +02:00
Antoine Bertin 69c5075ced Fix returned results of download_subtitles in api 2012-06-15 17:46:44 +02:00
Antoine Bertin 4da9b7080d Update services and unittests 2012-06-15 17:00:45 +02:00
Antoine Bertin be112bc091 Remove special languages from OpenSubtitles list (und, mis and mul) 2012-06-15 08:21:20 +02:00
Antoine Bertin 8389c86ce7 Rename test_set to test_set_contains in language unittests 2012-06-15 01:07:58 +02:00
Antoine Bertin fa55d32563 Update addic7ed, bierdopje and opensubtitles 2012-06-15 01:07:32 +02:00
Antoine Bertin 185cc9844c Update api, async, core and services to use the new language module 2012-06-15 01:06:31 +02:00
Antoine Bertin 705fb0d342 Init the session in the constructor of ServiceBase 2012-06-15 01:04:17 +02:00
Antoine Bertin 2ce9ac0862 Compact cache functions in ServiceBase 2012-06-15 01:03:40 +02:00
Antoine Bertin a237cd856d Use the NotImplementedError in ServiceBase 2012-06-15 01:02:34 +02:00
Antoine Bertin b1e685ffc2 Make the Video hashable 2012-06-15 01:01:39 +02:00
Antoine Bertin d19dde9843 Replace guessit.language in subtitles module 2012-06-15 01:01:01 +02:00
Antoine Bertin 74693bf747 Add language_list class and its unittests 2012-06-15 00:59:16 +02:00
Antoine Bertin 530bc7f5ff Update language module documentation and improve exception messages 2012-06-15 00:58:16 +02:00
Antoine Bertin 1b660d1e6d Fix language_set substraction operation and add a unittest 2012-06-15 00:56:31 +02:00
Antoine Bertin 34ce0d640c Fix language_set constructor with list of tuples 2012-06-15 00:53:20 +02:00
Antoine Bertin ae2c08bfbd Remove languages for special situations from the list 2012-06-15 00:51:53 +02:00
Antoine Bertin f517a683e3 Merge branch 'develop' of github.com:Diaoul/subliminal into develop 2012-06-12 23:36:02 +02:00
Antoine Bertin b512439d41 Remove some blank lines 2012-06-12 23:33:04 +02:00
Antoine Bertin c62a2a7672 Order documentation by source 2012-06-12 23:32:51 +02:00
Antoine Bertin bcd5c8f610 Add language stuff 2012-06-12 23:32:25 +02:00
Antoine Bertin a75aff65d6 Use skip in unittests 2012-06-09 23:46:36 +02:00
Antoine Bertin 9500f882d5 Replace required_parsers with required_features
Remove detection code and rely on beautifulsoup4 for that
2012-06-09 23:45:27 +02:00
Antoine Bertin 7fc5f2ef97 Update requirements 2012-06-09 19:33:54 +02:00
Antoine Bertin 5d0808a1f9 Merge branch 'develop' of git@github.com:Diaoul/subliminal.git into develop 2012-06-09 11:26:28 +02:00
Antoine Bertin 511fe7410b Update README 2012-06-08 10:57:45 +03:00
Antoine Bertin 7ed6819b03 Update unittests 2012-06-07 21:58:55 +02:00
Antoine Bertin 4709e26bd5 Update Podnapisi 2012-06-07 21:57:46 +02:00
Antoine Bertin 6e1fa561f0 Remove unused stuff in unittests 2012-06-07 18:11:09 +02:00
Antoine Bertin d7668d3573 Fix Podnapisi query method 2012-06-07 18:10:55 +02:00
Antoine Bertin 410295486a Update documentation 2012-06-07 18:09:30 +02:00
Antoine Bertin 0d63b47560 Clean up unused stuff in unittests 2012-06-03 22:05:41 +02:00
Antoine Bertin d3cb956061 Use the pythonic syntax for not in list 2012-06-03 22:04:54 +02:00
Antoine Bertin 2e00accfab Add Podnapisi service 2012-06-03 22:04:15 +02:00
Antoine Bertin c54a60097c Fix requirements 2012-06-03 15:02:23 +02:00
Antoine Bertin 58a54bce06 Merge pull request #84 from wackou/develop
Fixes for TvSubtitles and OpenSubtitles services
2012-06-03 05:15:32 -07:00
Antoine Bertin d00e6905e1 Fix python 2.6 compatibility
list.copy() is 2.7+
2012-06-03 14:10:36 +02:00
Nicolas Wack c7388c9247 Updated requirements for GuessIt 2012-05-12 17:30:30 +02:00
Nicolas Wack 5a2ab412b8 Fixed TvSubtitles download (we get back zip files, not srt ones) 2012-05-12 16:37:25 +02:00
Nicolas Wack 584acb0856 Updated OpenSubtitles' language support 2012-05-12 16:18:07 +02:00
Nicolas Wack 122f41507a Service.check_validity requires languages to be a set 2012-05-09 01:33:22 +02:00
Antoine Bertin 6a78564460 Add optional requirements to travis-ci configuration 2012-05-06 15:59:26 +02:00
Antoine Bertin 096cd5e09c Rename parser related variables. Clean up 2012-05-06 15:51:24 +02:00
Antoine Bertin cc3fa4b11a Update requirements 2012-05-06 12:08:34 +02:00
Antoine Bertin f050687487 Add automatic parser detection for BeautifulSoup 2012-05-06 11:40:12 +02:00
Antoine Bertin 4165ed0b9e Bump version to 0.6 in services 2012-05-06 11:39:23 +02:00
Antoine Bertin 3032590b8e Reorder imports 2012-05-06 11:38:36 +02:00
Antoine Bertin 113b504057 Use unicode in logging messages 2012-05-06 11:36:53 +02:00
Antoine Bertin 1a49f0b3ab Update documentation to remove references to subliminal.languages 2012-05-05 23:30:03 +02:00
Antoine Bertin a79143644b Fix import errors and be pep8 compliant 2012-05-05 23:12:02 +02:00
Antoine Bertin a2559d2d31 Add tests to setup.py and fix travis-ci support
Run tests with python setup.py test
2012-05-05 20:25:00 +02:00
Antoine Bertin 057933d737 Add support for travis-ci 2012-05-05 20:01:45 +02:00
Antoine Bertin bb16df5770 Change message when CLI does not download subtitles 2012-05-05 19:46:45 +02:00
Antoine Bertin b9c8ac23cc Fix requirements 2012-05-05 19:42:15 +02:00
Antoine Bertin bad6f77a01 Merge branch 'develop' of https://github.com/wackou/subliminal into develop 2012-05-05 19:42:07 +02:00
Nicolas Wack bb28945eba Optimized scanning for subtitles 2012-05-04 00:50:59 +02:00
Nicolas Wack 5a6ed82c5f Fixed possibly too greedy regexp 2012-05-03 23:10:21 +02:00
Nicolas Wack 055b2c7139 Fixed error message when no subtitles can be found in zipfile 2012-05-03 22:58:37 +02:00
Nicolas Wack e049eebeb0 Updated requirements for GuessIt 2012-04-28 20:56:33 +02:00
Nicolas Wack 0f3c68b7ef Removed bs4wrapper (requires BeautifulSoup>=4 now) 2012-04-28 20:23:39 +02:00
Nicolas Wack 607efff342 Merge remote-tracking branch 'olifozzy/develop' into languages_refactor 2012-04-25 02:14:14 +02:00
Nicolas Wack a88c05f7c3 Finished switching everything to guessit.Language; removed old language helper functions 2012-04-25 02:05:43 +02:00
Nicolas Wack 3aabbfde4c Subliminal now uses guessit.Language internally mostly everywhere 2012-04-24 22:25:21 +02:00
Nicolas Wack b447825a42 More flexilbe language handling 2012-04-22 03:28:12 +02:00
Nicolas Wack 2a1fb7bcb7 Unittests now clean up after themselves 2012-04-22 01:28:37 +02:00
Nicolas Wack fa7211c8b6 Added check on the size of the downloaded subtitle to make sure it is correct 2012-04-22 01:15:43 +02:00
Nicolas Wack bfa961a005 added unittests for the caching system 2012-04-21 21:43:08 +02:00
Nicolas Wack 4d9855d91c Refactored a bit unittests to use the generic EpisodeServiceTestCase 2012-04-21 20:09:04 +02:00
Nicolas Wack b09c069af8 Changed order of parameters for BierDopje.query() so that it follows the same order as the other services 2012-04-21 19:27:09 +02:00
Antoine Bertin 8fdb241a72 Update documentation 2012-04-15 23:25:14 +02:00
Nicolas Wack de781d0eec Refactored a bit the service unittests to avoid code duplication 2012-04-13 01:56:49 +02:00
Antoine Bertin 2959f52099 Fix CLI when --age is not given 2012-04-12 16:01:09 +03:00
Antoine Bertin 1e11c02006 Fix requirements for python 2.7
Add a requirement exception for previous versions of python
2012-04-11 22:24:37 +02:00
Antoine Bertin 12a58b0ca3 Bump version 2012-04-11 22:20:53 +02:00
Antoine Bertin 27f521d74e Update documentation 2012-04-11 21:15:02 +02:00
Antoine Bertin bbc3472873 Fix description for --age option in CLI 2012-04-11 20:35:47 +02:00
Antoine Bertin 680c699dfa Add --age option in CLI 2012-04-11 20:28:48 +02:00
Antoine Bertin b7ba6d51e2 Fix --workers option in CLI 2012-04-11 20:28:34 +02:00
Nicolas Wack 1f2128999f Correctly set config from a task to its cached service before consuming said task 2012-04-11 20:19:45 +02:00
olifozzy f03503b7f8 Change copyright 2012-04-11 09:53:00 +02:00
Antoine Bertin ae1b86173e Fix missing scan_filter option in async 2012-04-11 00:09:17 +02:00
Antoine Bertin e783c255b7 Add scan_filter option to filter out some paths 2012-04-10 23:48:49 +02:00
Nicolas Wack 0f86d7321f reworked caching system so that each ServiceConfig has its own Cache instance 2012-04-10 23:45:31 +02:00
Antoine Bertin f085b0fc8e Add argparse to the requirements (for python 2.6) 2012-04-10 22:07:57 +02:00
Nicolas Wack b4beba284d Caching system now has a separate cache and cache file for each service 2012-04-09 21:10:09 +02:00
Nicolas Wack c1e70e9e21 added TvSubtitles to the list of services in subliminal/core.py 2012-04-09 19:29:20 +02:00
olifozzy fca2c698fc Add unit tests 2012-04-06 15:09:35 +02:00
olifozzy 5d5b23f907 Change copyright author 2012-04-06 09:37:55 +02:00
olifozzy 2860e58830 Add Addic7ed service. 2012-04-06 01:06:26 +02:00
Nicolas Wack 014c3249b5 Much better TvSubtitles service; uses cache now too 2012-03-21 20:14:24 +01:00
Nicolas Wack 59920a5537 Enhanced caching system 2012-03-21 16:27:26 +01:00
Nicolas Wack 8e04de4d65 Better BeautifulSoup4 wrapper 2012-03-20 20:31:54 +01:00
Nicolas Wack 3ff8ebea7d fixed some python 2.6 issues
some objects can be used in 2.7 as context managers but not in 2.6
2012-03-15 22:33:35 +01:00
Nicolas Wack 29096d6700 Merge branch 'develop' of github.com:wackou/subliminal into develop 2012-03-14 23:48:25 +01:00
Nicolas Wack 1c59fc829f added first version of a TvSubtitles service 2012-03-14 23:42:43 +01:00
Nicolas Wack ca5c0427b8 allow services to download an extract zip files 2012-03-14 22:30:38 +01:00
Nicolas Wack f1dd77bdfd Compatibility for BeautifulSoup 3 and 4
BeautifulSoup4 is imported by default if present.
2012-03-13 01:54:33 +01:00
Nicolas Wack 4d06316d22 ported services to BeautifulSoup4 2012-03-05 22:02:31 +01:00
70 changed files with 3141 additions and 4138 deletions
+5
View File
@@ -0,0 +1,5 @@
[report]
exclude_lines =
def __repr__
raise NotImplementedError
if __name__ == .__main__.:
+12 -4
View File
@@ -20,16 +20,24 @@ pip-log.txt
.coverage
.tox
#Translations
# Translations
*.mo
#Mr Developer
# Mr Developer
.mr.developer.cfg
#Pydev
# Pydev
.project
.pydevproject
.settings
#Sphinx
# Rope
.ropeproject
# Sphinx
docs/_build
# Subliminal unittests
subliminal/tests/*.srt
subliminal/tests/*_files
subliminal/tests/*_cache
+24
View File
@@ -0,0 +1,24 @@
language: python
python:
- "2.7"
install:
- pip install coveralls --use-mirrors
- pip install -r requirements.txt --use-mirrors
script:
- coverage run --source=subliminal setup.py test
after_success:
- coveralls
notifications:
email: false
irc:
channels:
- "irc.freenode.org#subliminal"
on_success: change
on_failure: always
use_notice: true
skip_join: true
-674
View File
@@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
-165
View File
@@ -1,165 +0,0 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
+127
View File
@@ -0,0 +1,127 @@
Changelog
=========
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.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:** not released yet
* Initial release
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 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.
+1 -1
View File
@@ -1 +1 @@
include COPYING COPYING.LESSER NEWS.rst README.rst
include LICENSE HISTORY.rst requirements.txt
-54
View File
@@ -1,54 +0,0 @@
News
====
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:** not released yet
* Initial release
+38 -25
View File
@@ -1,43 +1,56 @@
Subliminal
==========
Subliminal is a python library to search and download subtitles.
It comes with an easy to use CLI (command-line interface) suitable for direct use or cron jobs.
It uses video hashes and the powerful `guessit <http://guessit.readthedocs.org/>`_ library
that extracts informations from filenames or filepaths to ensure you have the best subtitles.
It also relies on `enzyme <https://github.com/Diaoul/enzyme>`_ to detect embedded subtitles
and avoid duplicates.
.. image:: https://travis-ci.org/Diaoul/subliminal.png?branch=develop
:target: https://travis-ci.org/Diaoul/subliminal
Features
--------
Multiple subtitles services are available:
.. image:: https://coveralls.io/repos/Diaoul/subliminal/badge.png?branch=develop
:target: https://coveralls.io/r/Diaoul/subliminal?branch=develop
Providers
---------
Subliminal uses multiple providers to give users a vast choice and have a better chance to find the
best matching subtitles. Providers are extensible through a dedicated entry point.
* Addic7ed
* BierDopje
* OpenSubtitles
* TheSubDB
* BierDopje
* SubsWiki
* Subtitulos
* TvSubtitles
You can use main subliminal's functions with a **file path**, a **file name** or a **folder path**.
Usage
-----
CLI
^^^
Download english subtitles::
$ subliminal -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
**************************************************
Downloaded 1 subtitles
(Episode(u'The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4'), ResultSubtitle(en, opensubtitles, 0.33, The.Big.Bang.Theory.S05E18.HDTV-LOL.srt))
**************************************************
$ subliminal -l en -- The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
1 subtitle downloaded
Module
^^^^^^
List english subtitles::
Library
^^^^^^^
Download best subtitles in French and English for videos less than one week old in a video folder,
skipping videos that already have subtitles whether they are embedded or not::
>>> subliminal.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
from babelfish import Language
from datetime import timedelta
import subliminal
# configure the cache
subliminal.cache_region.configure('dogpile.cache.dbm', arguments={'filename': '/path/to/cachefile.dbm'})
Multi-threaded use
^^^^^^^^^^^^^^^^^^
Use 4 workers to achieve the same result::
# scan for videos in the folder and their subtitles
videos = scan_videos(['/path/to/video/folder'], subtitles=True, embedded_subtitles=True)
>>> with subliminal.Pool(4) as p:
... p.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
# download
subliminal.download_best_subtitles(videos, {Language('eng'), Language('fra')}, age=timedelta(week=1))
License
-------
MIT
+4
View File
@@ -0,0 +1,4 @@
sympy>=0.7.3
sphinx>=1.1.3
sphinxcontrib-programoutput>=0.8
Sphinx-PyPI-upload>=0.2.1
+25 -1
View File
@@ -7,6 +7,11 @@ SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
@@ -29,17 +34,20 @@ help:
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@@ -108,6 +116,12 @@ latexpdf:
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@@ -151,3 +165,13 @@ doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

+2 -2
View File
@@ -1,4 +1,4 @@
<h3>About</h3>
<h3>Subliminal</h3>
<p>
Subliminal is a python library to search and download subtitles.
Subliminal is a Python library to search and download subtitles.
</p>
-6
View File
@@ -1,6 +0,0 @@
<h3>Useful Links</h3>
<ul>
<li><a href="http://pypi.python.org/pypi/subliminal">subliminal @ PyPI</a></li>
<li><a href="http://github.com/Diaoul/subliminal">subliminal @ GitHub</a></li>
<li><a href="http://github.com/Diaoul/subliminal/issues">Issue Tracker</a></li>
</ul>
-4
View File
@@ -1,4 +0,0 @@
<p>
<iframe src="http://markdotto.github.com/github-buttons/github-btn.html?user=Diaoul&repo=subliminal&type=watch&count=true&size=large"
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
</p>
+8
View File
@@ -0,0 +1,8 @@
API
===
.. module:: subliminal.api
.. autodata:: PROVIDERS_ENTRY_POINT
.. autofunction:: list_subtitles
.. autofunction:: download_subtitles
.. autofunction:: download_best_subtitles
+7
View File
@@ -0,0 +1,7 @@
Cache
=====
.. module:: subliminal.cache
.. autodata:: region
Refer to `dogpile.cache's documentation <http://dogpilecache.readthedocs.org>`_ to see how to configure a region
+7
View File
@@ -0,0 +1,7 @@
CLI
===
.. module:: subliminal.cli
subliminal
----------
.. program-output:: subliminal --help
+9
View File
@@ -0,0 +1,9 @@
Exceptions
==========
.. module:: subliminal.exceptions
.. autoclass:: Error
.. autoclass:: ProviderError
.. autoclass:: ProviderConfigurationError
.. autoclass:: ProviderNotAvailable
.. autoclass:: InvalidSubtitle
+6
View File
@@ -0,0 +1,6 @@
Providers
=========
.. module:: subliminal.providers
.. autoclass:: Provider
:members:
+6
View File
@@ -0,0 +1,6 @@
Score
=====
.. module:: subliminal.score
.. autofunction:: get_episode_equations
.. autofunction:: get_movie_equations
+9
View File
@@ -0,0 +1,9 @@
Subtitle
========
.. module:: subliminal.subtitle
.. autoclass:: Subtitle
:members:
.. autofunction:: get_subtitle_path
.. autofunction:: is_valid_subtitle
.. autofunction:: compute_guess_matches
+17
View File
@@ -0,0 +1,17 @@
Video
=====
.. module:: subliminal.video
.. autodata:: VIDEO_EXTENSIONS
.. autodata:: SUBTITLE_EXTENSIONS
.. autoclass:: Video
:members:
.. autoclass:: Episode
:members:
.. autoclass:: Movie
:members:
.. autofunction:: hash_opensubtitles
.. autofunction:: hash_thesubdb
.. autofunction:: scan_subtitle_languages
.. autofunction:: scan_video
.. autofunction:: scan_videos
+35 -13
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# subliminal documentation build configuration file, created by
# sphinx-quickstart on Tue Feb 28 16:33:06 2012.
# sphinx-quickstart on Wed Oct 23 23:24:28 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
@@ -18,7 +18,7 @@ import sys, os
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('..'))
sys.path.append(os.path.abspath('_themes'))
import subliminal.infos
import subliminal
# -- General configuration -----------------------------------------------------
@@ -27,7 +27,7 @@ import subliminal.infos
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc']
extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.programoutput']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -42,17 +42,17 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = u'subliminal'
copyright = u'2012, Antoine Bertin'
project = subliminal.__title__
copyright = ' '.join(subliminal.__copyright__.split()[1:])
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = subliminal.infos.__version__
version = subliminal.__version__
# The full version, including alpha/beta/rc tags.
release = version
release = subliminal.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -88,17 +88,29 @@ pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'flask'
html_theme = 'diaoul'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
html_theme_options = {'github_user': 'Diaoul',
'github_repo': 'subliminal',
'github_branch': 'master',
'fork_me': 1,
'flattr': 0,
'gittip': 'Diaoul',
'pypi_downloads': 1,
'pypi_version': 0,
'travis': 0,
'coveralls': 0}
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = ['_themes']
@@ -112,7 +124,7 @@ html_theme_path = ['_themes']
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
html_logo = '_static/subliminal-logo.png'
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
@@ -134,9 +146,11 @@ html_static_path = ['_static']
# Custom sidebar templates, maps document names to template names.
html_sidebars = {
'index': ['sidebar-intro.html', 'sidebar-watch.html', 'localtoc.html', 'sidebar-links.html', 'searchbox.html'],
'**': ['localtoc.html', 'relations.html', 'sourcelink.html']
}
'index': ['sidebar-intro.html', 'sidebar-star.html', 'sidebar-pypi.html', 'sidebar-donate.html',
'sourcelink.html', 'searchbox.html'],
'**': ['sidebar-intro.html', 'sidebar-star.html', 'sidebar-pypi.html', 'sidebar-donate.html',
'localtoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html']
}
# Additional templates that should be rendered to pages, maps page names to
# template names.
@@ -245,3 +259,11 @@ texinfo_documents = [
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# -- Options for autodoc -------------------------------------------------------
autodoc_member_order = 'bysource'
-87
View File
@@ -1,87 +0,0 @@
This guide is going to explain the main logic of subliminal and detail
every class or function.
Services
--------
Subliminal aims at downloading subtitles. Over the web, one can find subtitles
combining different websites but there is no guarantee of a perfect match.
Even if OpenSubtitles has a gigantic subtitles database, you may not be able to
find a subtitle on it but you will find it elsewhere, say BierDopje. Sometimes,
it just takes some time before it shows up on a website even if already available
on another, but you don't wanna wait to watch the latest Big Bang Theory, right?
Given this, to be reliable, subliminal has to use different :mod:`~subliminal.services`
and use a unified method to gather them all. The :class:`~subliminal.services.ServiceBase`
class will achieve this.
.. automodule:: subliminal.services
:members:
Languages
---------
To be able to support many languages, subliminal has a :mod:`~subliminal.languages`
module that contains utility functions and ISO languages code (639-1 and 639-2)
.. automodule:: subliminal.languages
:members:
Tasks
-----
Subliminal is IO bound: it mostly waits for IO operations (web requests) to complete.
Thus, subliminal is a good place for multi-threading. It works with atomic operations
represented by a :class:`~subliminal.tasks.Task` class which can be consumed with
:func:`~subliminal.core.consume_task` but we'll see that later.
.. automodule:: subliminal.tasks
:members:
Asynchronous
------------
To consume those tasks in an asynchronous way without flooding services with requests,
subliminal uses multiple instances of the :class:`~subliminal.async.Worker` class that
will consume the same task queue. Each worker will only create a single instance of each
:mod:`service <subliminal.services>` and this save some initialization time.
The :class:`~subliminal.async.Pool` is here to instantiate and manage multiple workers
at a time.
.. automodule:: subliminal.async
:members:
Core
----
The goal of subliminal's :mod:`~subliminal.core` module is to merge results from
consumed tasks. Merging has to be intelligent and take user preferences into account.
Core module is thus responsible for the computation of a :func:`matching confidence
<subliminal.core.matching_confidence>` so the user knows the chances that the
:class:`~subliminal.subtitles.ResultSubtitle` matches the :class:`~subliminal.videos.Video`
.. automodule:: subliminal.core
:members:
Other objects
-------------
Subliminal uses some other self-explanatory functions and classes listed below.
Video
^^^^^
.. automodule:: subliminal.videos
:members:
Subtitle
^^^^^^^^
.. automodule:: subliminal.subtitles
:members:
Utilities
^^^^^^^^^
.. automodule:: subliminal.utils
:members:
Exceptions
^^^^^^^^^^
.. automodule:: subliminal.exceptions
:members:
+69 -44
View File
@@ -1,5 +1,5 @@
.. subliminal documentation master file, created by
sphinx-quickstart on Tue Feb 28 16:33:06 2012.
sphinx-quickstart on Wed Oct 23 23:24:28 2013.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
@@ -8,70 +8,95 @@ Subliminal
Release v\ |version|
Subliminal is a python library to search and download subtitles.
It comes with an easy to use :abbr:`CLI (command-line interface)` suitable for direct use or cron jobs.
It uses video hashes and the powerful `guessit <http://guessit.readthedocs.org/>`_ library
that extracts informations from filenames or filepaths to ensure you have the best subtitles.
It also relies on `enzyme <https://github.com/Diaoul/enzyme>`_ to detect embedded subtitles
and avoid duplicates.
Features
--------
Multiple subtitles services are available:
Providers
---------
Subliminal uses multiple providers to give users a vast choice and have a better chance to find the
best matching subtitles. Providers are extensible through a dedicated entry point.
* Addic7ed
* BierDopje
* OpenSubtitles
* TheSubDB
* BierDopje
* SubsWiki
* Subtitulos
* TvSubtitles
You can use main subliminal's functions with a **file path**, a **file name** or a **folder path**.
Usage
-----
CLI
^^^
Download english subtitles::
$ subliminal -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
**************************************************
Downloaded 1 subtitles
(Episode(u'The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4'), ResultSubtitle(en, opensubtitles, 0.33, The.Big.Bang.Theory.S05E18.HDTV-LOL.srt))
**************************************************
$ subliminal -l en -- The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
1 subtitle downloaded
Module
^^^^^^
List english subtitles::
See :mod:`subliminal.cli`
>>> subliminal.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
Library
^^^^^^^
Download best subtitles in French and English for videos less than one week old in a video folder,
skipping videos that already have subtitles whether they are embedded or not::
Multi-threaded use
^^^^^^^^^^^^^^^^^^
Use 4 workers to achieve the same result::
from babelfish import Language
from datetime import timedelta
import subliminal
# configure the cache
subliminal.cache_region.configure('dogpile.cache.dbm', arguments={'filename': '/path/to/cachefile.dbm'})
>>> with subliminal.Pool(4) as p:
... p.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
# scan for videos in the folder and their subtitles
videos = scan_videos(['/path/to/video/folder'], subtitles=True, embedded_subtitles=True)
User Guide
----------
This part of the documentation details how to use subliminal for most common tasks
# download
subliminal.download_best_subtitles(videos, {Language('eng'), Language('fra')}, age=timedelta(week=1))
See :mod:`subliminal.api`, :func:`~subliminal.video.scan_videos` and :func:`~subliminal.video.scan_video`
How it works
------------
Subliminal makes use of various libraries to achieve its goal:
* `enzyme <http://enzyme.readthedocs.org>`_ to detect embedded subtitles in videos and retrieve metadata
* `guessit <http://guessit.readthedocs.org>`_ to guess informations from filenames
* `babelfish <http://babelfish.readthedocs.org>`_ to work with languages
* `requests <http://docs.python-requests.org>`_ to make human readable HTTP requests
* `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup>`_ to parse HTML and XML
* `dogpile.cache <http://dogpilecache.readthedocs.org>`_ to cache intermediate search data
* `charade <https://github.com/sigmavirus24/charade>`_ to detect subtitles' encoding
* `pysrt <https://github.com/byroot/pysrt>`_ to validate downloaded subtitles
License
-------
MIT
Documentation
-------------
.. toctree::
:maxdepth: 2
user
:maxdepth: 2
Developer Guide
---------------
This part of the documentation explains internal behavior of subliminal and its algorithms
.. toctree::
:maxdepth: 2
dev
provider_guide
API Documentation
-----------------
Most common subliminal features are listed here
If you are looking for information on a specific function, class or method,
this part of the documentation is for you.
.. automodule:: subliminal
:members:
:noindex:
.. toctree::
:maxdepth: 2
api/api
api/cache
api/cli
api/exceptions
api/providers
api/score
api/subtitle
api/video
.. include:: ../HISTORY.rst
+103
View File
@@ -0,0 +1,103 @@
Provider Guide
==============
This guide is going to explain how to add a :class:`~subliminal.providers.Provider` to subliminal
Requirements
------------
When starting a provider you should be able to answer to the following questions:
* What languages does my provider support?
* What are the language codes for the supported languages?
* Does my provider deliver subtitles for episodes? for movies?
* Does my provider require a video hash?
Each response of these questions will help you set the correct attributes for your
:class:`~subliminal.providers.Provider`.
Video Validation
----------------
Not all providers deliver subtitles for :class:`~subliminal.video.Episode`. Some may require a hash.
The :meth:`~subliminal.providers.Provider.check` method does validation against a :class:`~subliminal.video.Video`
object and will return `False` if the given :class:`~subliminal.video.Video` isn't suitable. If you're not happy
with the default implementation, you can override it.
Configuration
-------------
API keys must not be configurable by the user and must remain linked to subliminal. Hence they must be written
in the provider module.
Per-user authentication is allowed and must be configured at instantiation as keyword arguments. Configuration
will be done by the user through the `provider_configs` argument of the :func:`~subliminal.api.list_subtitles` and
:func:`~subliminal.api.download_best_subtitles` functions. No network operation must be done during instantiation,
only configuration. Any error in the configuration must raise a
:class:`~subliminal.exceptions.ProviderConfigurationError`.
Beyond this point, if a network error occurs, a :class:`~subliminal.exceptions.ProviderNotAvailable` exception
must be raised and an unexpected behavior must raise a :class:`~subliminal.exceptions.ProviderError` exception.
Initialization / Termination
----------------------------
Actual authentication operations must take place in the :meth:`~subliminal.providers.Provider.initialize` method.
If you need anything to be executed when the provider isn't used anymore like logout,
use :meth:`~subliminal.providers.Provider.terminate`.
Caching policy
--------------
To save bandwidth and improve querying time, intermediate data should be cached when possible. Typical use case is
when a query to retrieve show ids is required prior to the query to actually search for subtitles. In that case
the function that gets the show id from the show name must be cached.
Language
--------
To be able to handle various language codes, subliminal makes use of `babelfish <http://babelfish.readthedocs.org>`_
Language and converters. You must set the attribute :attr:`~subliminal.providers.Provider.languages` with a set of
supported :class:`babelfish.Language`.
If you cannot find a suitable converter for your provider, you can `make one of your own
<http://babelfish.readthedocs.org/en/latest/#custom-converters>`_.
Querying
--------
The :meth:`~subliminal.providers.Provider.query` method parameters must include all aspects of provider's querying with
simple types.
Subtitle
--------
A custom :class:`~subliminal.subtitle.Subtitle` subclass must be created to represent a subtitle from the provider.
It must have relevant attributes that can be used to compute the matches of the subtitle against a
:class:`~subliminal.video.Video` object.
Score computation
-----------------
To be able to compare subtitles coming from different providers between them, the
:meth:`~subliminal.subtitle.Subtitle.compute_matches` method must be implemented.
If `guessit <http://guessit.readthedocs.org>`_ is used to extract data from the
:class:`~subliminal.subtitle.Subtitle` subclass, you can use :func:`~subliminal.subtitle.compute_guess_matches`
as a helper to compute matches between the :class:`~subliminal.video.Video` and the :class:`guessit.Guess`.
Refer to the `scores` attribute of :class:`~subliminal.video.Episode` and :class:`~subliminal.video.Movie`
for a list of possible matches.
Unittesting
-----------
All possible uses of the :meth:`~subliminal.providers.Provider.query` method must be unittested including the uses
that produce exceptions other than :class:`~subliminal.exceptions.ProviderNotAvailable`.
The :meth:`~subliminal.subtitle.Subtitle.compute_matches` is used to validate the unittests.
As it is not possible to unittest all uses of the :meth:`~subliminal.providers.Provider.list_subtitles`
and :meth:`~subliminal.providers.Provider.download_subtitle` methods, unitests are only required to cover most common
use cases.
See existing unittests for more details on how to proceed.
-113
View File
@@ -1,113 +0,0 @@
There are 4 different ways of using subliminal and each one
is described in a dedicated section below.
First, here are some basics
Basics
------
Services
^^^^^^^^
You can use subliminal with multiple services to get the best result.
Current available services are available in the :data:`subliminal.SERVICES` variable.
.. autodata:: subliminal.SERVICES
Languages
^^^^^^^^^
Subliminal supports multiple languages that are represented with their extended ISO 639-1 code.
The current extensions to the ISO 639-1 are:
* *po* for Brazilian Portuguese
Paths
^^^^^
All paths parameters in subliminal most commont functions can be either *a file path*,
*a file name* or a *folder path*
* File path (existing): hashes of the file will be computed and used during the search for services that supports
this functionnality.
* File name (or non-existing file path): the guessit python library will be used to guess informations and a text-based search will
be done with services.
* Folder path (containing video files): the given folder will be searched for video files using their :data:`~subliminal.videos.MIMETYPES`
and/or :data:`~subliminal.videos.EXTENSIONS`. The default maximum depth to scan is 3
CLI
---
Subliminal is shipped with a basic Command Line Interface that allows you to
download subtitles for one or more videos in a multi-threaded way.
You can have the documentation of the CLI using ``subliminal --help``::
usage: subliminal [-h] [-l LG] [-s NAME] [-m] [-f] [-w N] [-c] [-q | -v]
[--cache-dir DIR | --no-cache-dir] [--version]
PATH [PATH ...]
Subtitles, faster than your thoughts
positional arguments:
PATH path to video file or folder
optional arguments:
-h, --help show this help message and exit
-l LG, --language LG wanted language (ISO 639-1)
-s NAME, --service NAME
service to use
-m, --multi download multiple subtitle languages
-f, --force replace existing subtitle file
-w N, --workers N use N threads (default: 4)
-c, --compatibility try not to use unicode (use this if you have encoding
errors)
-q, --quiet disable output
-v, --verbose verbose output
--cache-dir DIR cache directory to use
--no-cache-dir do not use cache directory (some services may not
work)
--version show program's version number and exit
.. note::
The cache directory defaults to *~/.config/subliminal*. Even on Windows
Simple module use
-----------------
Subliminal comes with two basic functions to search and download subtitles. For example, you
can do::
>>> subliminal.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
.. autofunction:: subliminal.list_subtitles
Or even download missing subtitles for each episodes under the given folders in two different languages::
>>> subliminal.download_subtitles(['/mnt/videos/BBT/Season 05', '/mnt/videos/HIMYM/Season 07'],
... ['en', 'fr'], force=False, multi=True)
.. autofunction:: subliminal.download_subtitles
Multi-threaded module use
-------------------------
You can call the same functions on a :class:`subliminal.Pool` object previously
created with the appropriate number of workers.
.. autoclass:: subliminal.Pool
:members:
You have to call the :meth:`~subliminal.Pool.start` method before any actions and
:meth:`~subliminal.Pool.stop` before exiting your program::
>>> p = subliminal.Pool(4)
... p.start()
... p.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
... p.stop()
To make the use of :class:`~subliminal.Pool` easier, you can use the ``with`` statement
that takes care of that for you::
>>> with subliminal.Pool(4) as p:
... p.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
* from the command line
* basic functions :func:`~subliminal.list_subtitles` and :func:`~subliminal.download_subtitles`
* multi-threaded :class:`~subliminal.async.Pool` which implements the abovementioned functions
* using your own algorithm that produces and gather results of elementary :class:`~subliminal.tasks.Task`
+9 -4
View File
@@ -1,4 +1,9 @@
BeautifulSoup>=3.2.0
guessit>=0.2
requests
enzyme>=0.1
beautifulsoup4>=4.3.2
guessit>=0.6.1
requests>=2.0.1
enzyme>=0.4.0
html5lib>=0.99
dogpile.cache>=0.5.1
babelfish>=0.2.1
charade>=1.0.3
pysrt>=0.5.0
-79
View File
@@ -1,79 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
import argparse
import subliminal
import logging
import os
import sys
def main():
parser = argparse.ArgumentParser(description='Subtitles, faster than your thoughts')
parser.add_argument('-l', '--language', action='append', dest='languages', help='wanted language (ISO 639-1)', metavar='LG')
parser.add_argument('-s', '--service', action='append', dest='services', help='service to use', metavar='NAME')
parser.add_argument('-m', '--multi', action='store_true', help='download multiple subtitle languages')
parser.add_argument('-f', '--force', action='store_true', help='replace existing subtitle file')
parser.add_argument('-w', '--workers', action='store', help='use N threads (default: %(default)s)', metavar='N', default=4)
parser.add_argument('-c', '--compatibility', action='store_true', help='try not to use unicode (use this if you have encoding errors)')
group_verbosity = parser.add_mutually_exclusive_group()
group_verbosity.add_argument('-q', '--quiet', action='store_true', help='disable output')
group_verbosity.add_argument('-v', '--verbose', action='store_true', help='verbose output')
group_cache = parser.add_mutually_exclusive_group()
group_cache.add_argument('--cache-dir', action='store', dest='cache_dir', help='cache directory to use', metavar='DIR', default=os.path.expanduser('~/.config/subliminal'))
group_cache.add_argument('--no-cache-dir', action='store_false', dest='cache_dir', help='do not use cache directory (some services may not work)')
parser.add_argument('--version', action='version', version=subliminal.__version__)
parser.add_argument('paths', nargs='+', help='path to video file or folder', metavar='PATH')
args = parser.parse_args()
# Set log verbosity
if args.verbose:
logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(asctime)s %(name)-24s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
elif not args.quiet:
logging.basicConfig(level=logging.WARN, format='%(levelname)s: %(name)s %(message)s')
# Create cache directory
if not os.path.exists(args.cache_dir):
os.mkdir(args.cache_dir)
# Compatibility mode
if args.compatibility:
paths = args.paths
else:
paths = [unicode(x) for x in args.paths]
# Download subtitles
with subliminal.Pool(args.workers) as p:
subtitles = p.download_subtitles(paths, languages=args.languages,
services=args.services, cache_dir=args.cache_dir,
force=args.force, multi=args.multi)
if not subtitles:
if not args.quiet:
sys.stderr.write('No subtitles found\n')
exit(1)
if not args.quiet:
print '*' * 50
print 'Downloaded %d subtitles' % len(subtitles)
for subtitle in subtitles:
print subtitle
print '*' * 50
if __name__ == '__main__':
main()
+27 -35
View File
@@ -1,46 +1,38 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
import os.path
from setuptools import setup, find_packages
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
execfile(os.path.join(os.path.dirname(__file__), 'subliminal', 'infos.py'))
setup(name='subliminal',
version=__version__,
license='LGPLv3',
version='0.7.1',
license='MIT',
description='Subtitles, faster than your thoughts',
long_description=read('README.rst') + '\n\n' + read('NEWS.rst'),
classifiers=['Development Status :: 4 - Beta',
'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
'Intended Audience :: Developers',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Multimedia :: Video'],
long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(),
keywords='subtitle subtitles video movie episode tv show',
url='https://github.com/Diaoul/subliminal',
author='Antoine Bertin',
author_email='diaoulael@gmail.com',
url='https://github.com/Diaoul/subliminal',
packages=find_packages(),
scripts=['scripts/subliminal'],
install_requires=['BeautifulSoup >= 3.2.0', 'guessit >= 0.2', 'requests', 'enzyme >= 0.1'])
classifiers=['Development Status :: 4 - Beta',
'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',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Multimedia :: Video'],
entry_points={
'console_scripts': ['subliminal = subliminal.cli:subliminal'],
'subliminal.providers': ['addic7ed = subliminal.providers.addic7ed:Addic7edProvider',
'bierdopje = subliminal.providers.bierdopje:BierDopjeProvider',
'opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider',
'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider',
'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider'],
'babelfish.converters': ['addic7ed = subliminal.converters.addic7ed:Addic7edConverter',
'tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter']
},
install_requires=open('requirements.txt').readlines(),
test_suite='subliminal.tests.suite')
+12 -30
View File
@@ -1,34 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from .api import list_subtitles, download_subtitles
from .async import Pool
from .core import (SERVICES, LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE,
MATCHING_CONFIDENCE)
from .infos import __version__
__title__ = 'subliminal'
__version__ = '0.7.1'
__author__ = 'Antoine Bertin'
__license__ = 'MIT'
__copyright__ = 'Copyright 2013 Antoine Bertin'
import logging
try:
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record):
pass
from .api import PROVIDERS_ENTRY_POINT, list_subtitles, download_subtitles, download_best_subtitles
from .cache import region as cache_region
from .exceptions import Error, ProviderError, ProviderConfigurationError, ProviderNotAvailable, InvalidSubtitle
from .subtitle import Subtitle
from .video import VIDEO_EXTENSIONS, SUBTITLE_EXTENSIONS, Video, Episode, Movie, scan_videos, scan_video
__all__ = ['SERVICES', 'LANGUAGE_INDEX', 'SERVICE_INDEX', 'SERVICE_CONFIDENCE',
'MATCHING_CONFIDENCE', 'list_subtitles', 'download_subtitles', 'Pool']
logging.getLogger(__name__).addHandler(NullHandler())
logging.getLogger(__name__).addHandler(logging.NullHandler())
+258 -82
View File
@@ -1,100 +1,276 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from .core import (SERVICES, LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE,
MATCHING_CONFIDENCE, create_list_tasks, consume_task, create_download_tasks,
group_by_video, key_subtitles)
from .languages import list_languages
from __future__ import unicode_literals
import collections
import io
import logging
import operator
import babelfish
import pkg_resources
from .exceptions import ProviderNotAvailable, InvalidSubtitle
from .subtitle import get_subtitle_path
__all__ = ['list_subtitles', 'download_subtitles']
logger = logging.getLogger(__name__)
#: Entry point for the providers
PROVIDERS_ENTRY_POINT = 'subliminal.providers'
def list_subtitles(paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3):
"""List subtitles in given paths according to the criteria
:param paths: path(s) to video file or folder
:type paths: string or list
:param list languages: languages to search for, in preferred order
:param list services: services to use for the search, in preferred order
:param bool force: force searching for subtitles even if some are detected
:param bool multi: search multiple languages for the same video
:param string cache_dir: path to the cache directory to use
:param int max_depth: maximum depth for scanning entries
def list_subtitles(videos, languages, providers=None, provider_configs=None):
"""List subtitles for `videos` with the given `languages` using the specified `providers`
:param videos: videos to list subtitles for
:type videos: set of :class:`~subliminal.video.Video`
:param languages: languages of subtitles to search for
:type languages: set of :class:`babelfish.Language`
:param providers: providers to use for the search, if not all
:type providers: list of string or None
:param provider_configs: configuration for providers
:type provider_configs: dict of provider name => provider constructor kwargs
:return: found subtitles
:rtype: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.ResultSubtitle`]
:rtype: dict of :class:`~subliminal.video.Video` => [:class:`~subliminal.subtitle.Subtitle`]
"""
services = services or SERVICES
languages = set(languages or list_languages(1))
if isinstance(paths, basestring):
paths = [paths]
if any([not isinstance(p, unicode) for p in paths]):
logger.warning(u'Not all entries are unicode')
results = []
service_instances = {}
tasks = create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth)
for task in tasks:
provider_configs = provider_configs or {}
subtitles = collections.defaultdict(list)
# filter videos
videos = [v for v in videos if v.subtitle_languages & languages < languages]
if not videos:
logger.info('No video to download subtitles for with languages %r', languages)
return subtitles
subtitle_languages = set.intersection(*[v.subtitle_languages for v in videos])
for provider_entry_point in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT):
# filter and initialize provider
if providers is not None and provider_entry_point.name not in providers:
logger.debug('Skipping provider %r: not in the list', provider_entry_point.name)
continue
Provider = provider_entry_point.load()
provider_languages = Provider.languages & languages - subtitle_languages
if not provider_languages:
logger.debug('Skipping provider %r: no language to search for', provider_entry_point.name)
continue
provider_videos = [v for v in videos if Provider.check(v)]
if not provider_videos:
logger.debug('Skipping provider %r: no video to search for', provider_entry_point.name)
continue
# list subtitles with the provider
try:
result = consume_task(task, service_instances)
results.append((task.video, result))
except:
logger.error(u'Error consuming task %r' % task, exc_info=True)
for service_instance in service_instances.itervalues():
service_instance.terminate()
return group_by_video(results)
with Provider(**provider_configs.get(provider_entry_point.name, {})) as provider:
for provider_video in provider_videos:
provider_video_languages = provider_languages - provider_video.subtitle_languages
if not provider_video_languages:
logger.debug('Skipping provider %r: no language to search for for video %r',
provider_entry_point.name, provider_video)
continue
logger.info('Listing subtitles with provider %r for video %r with languages %r',
provider_entry_point.name, provider_video, provider_video_languages)
try:
provider_subtitles = provider.list_subtitles(provider_video, provider_video_languages)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
break
except:
logger.exception('Unexpected error in provider %r', provider_entry_point.name)
continue
logger.info('Found %d subtitles', len(provider_subtitles))
subtitles[provider_video].extend(provider_subtitles)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
return subtitles
def download_subtitles(paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, order=None):
"""Download subtitles in given paths according to the criteria
def download_subtitles(subtitles, provider_configs=None, single=False):
"""Download subtitles
:param paths: path(s) to video file or folder
:type paths: string or list
:param list languages: languages to search for, in preferred order
:param list services: services to use for the search, in preferred order
:param bool force: force searching for subtitles even if some are detected
:param bool multi: search multiple languages for the same video
:param string cache_dir: path to the cache directory to use
:param int max_depth: maximum depth for scanning entries
:param order: preferred order for subtitles sorting
:type list: list of :data:`~subliminal.core.LANGUAGE_INDEX`, :data:`~subliminal.core.SERVICE_INDEX`, :data:`~subliminal.core.SERVICE_CONFIDENCE`, :data:`~subliminal.core.MATCHING_CONFIDENCE`
:return: found subtitles
:rtype: list of (:class:`~subliminal.videos.Video`, [:class:`~subliminal.subtitles.ResultSubtitle`])
:param subtitles: subtitles to download
:type subtitles: dict of :class:`~subliminal.video.Video` => [:class:`~subliminal.subtitle.Subtitle`]
:param provider_configs: configuration for providers
:type provider_configs: dict of provider name => provider constructor kwargs
:param bool single: download with .srt extension if `True`, add language identifier otherwise
"""
services = services or SERVICES
languages = languages or list_languages(1)
if isinstance(paths, basestring):
paths = [paths]
order = order or [LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE]
subtitles_by_video = list_subtitles(paths, set(languages), services, force, multi, cache_dir, max_depth)
for video, subtitles in subtitles_by_video.iteritems():
subtitles.sort(key=lambda s: key_subtitles(s, video, languages, services, order), reverse=True)
results = []
service_instances = {}
tasks = create_download_tasks(subtitles_by_video, multi)
for task in tasks:
provider_configs = provider_configs or {}
discarded_providers = set()
providers_by_name = {ep.name: ep.load() for ep in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT)}
initialized_providers = {}
try:
for video, video_subtitles in subtitles.items():
languages = {subtitle.language for subtitle in video_subtitles}
downloaded_languages = set()
for subtitle in video_subtitles:
# filter
if subtitle.language in downloaded_languages:
continue
if subtitle.provider_name in discarded_providers:
logger.debug('Skipping subtitle from discarded provider %r', subtitle.provider_name)
continue
# initialize provider
if subtitle.provider_name in initialized_providers:
provider = initialized_providers[subtitle.provider_name]
else:
provider = providers_by_name[subtitle.provider_name](**provider_configs.get(subtitle.provider_name, {}))
try:
provider.initialize()
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', subtitle.provider_name)
discarded_providers.add(subtitle.provider_name)
continue
initialized_providers[subtitle.provider_name] = provider
# download subtitles
subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language)
logger.info('Downloading subtitle %r into %r', subtitle, subtitle_path)
try:
subtitle_text = provider.download_subtitle(subtitle)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', subtitle.provider_name)
discarded_providers.add(subtitle.provider_name)
continue
except InvalidSubtitle:
logger.info('Invalid subtitle, skipping it')
continue
except:
logger.exception('Unexpected error in provider %r', subtitle.provider_name)
continue
with io.open(subtitle_path, 'w', encoding='utf-8') as f:
f.write(subtitle_text)
downloaded_languages.add(subtitle.language)
if single or downloaded_languages == languages:
break
finally: # terminate providers
for (provider_name, provider) in initialized_providers.items():
try:
provider.terminate()
except ProviderNotAvailable:
logger.warning('Provider %r is not available, unable to terminate', provider_name)
except:
logger.exception('Unexpected error in provider %r', provider_name)
def download_best_subtitles(videos, languages, providers=None, provider_configs=None, single=False, min_score=0,
hearing_impaired=False):
"""Download the best subtitles for `videos` with the given `languages` using the specified `providers`
:param videos: videos to download subtitles for
:type videos: set of :class:`~subliminal.video.Video`
:param languages: languages of subtitles to download
:type languages: set of :class:`babelfish.Language`
:param providers: providers to use for the search, if not all
:type providers: list of string or None
:param provider_configs: configuration for providers
:type provider_configs: dict of provider name => provider constructor kwargs
:param bool single: download with .srt extension if `True`, add language identifier otherwise
:param int min_score: minimum score for subtitles to download
:param bool hearing_impaired: download hearing impaired subtitles
"""
provider_configs = provider_configs or {}
discarded_providers = set()
downloaded_subtitles = collections.defaultdict(list)
# filter videos
videos = [v for v in videos if v.subtitle_languages & languages < languages
and (not single or babelfish.Language('und') not in v.subtitle_languages)]
if not videos:
logger.info('No video to download subtitles for with languages %r', languages)
return downloaded_subtitles
# filter and initialize providers
subtitle_languages = set.intersection(*[v.subtitle_languages for v in videos])
initialized_providers = {}
for provider_entry_point in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT):
if providers is not None and provider_entry_point.name not in providers:
logger.debug('Skipping provider %r: not in the list', provider_entry_point.name)
continue
Provider = provider_entry_point.load()
if not Provider.languages & languages - subtitle_languages:
logger.debug('Skipping provider %r: no language to search for', provider_entry_point.name)
continue
if not [v for v in videos if Provider.check(v)]:
logger.debug('Skipping provider %r: no video to search for', provider_entry_point.name)
continue
provider = Provider(**provider_configs.get(provider_entry_point.name, {}))
try:
result = consume_task(task, service_instances)
results.append(result)
except:
logger.error(u'Error consuming task %r' % task, exc_info=True)
for service_instance in service_instances.itervalues():
service_instance.terminate()
return results
provider.initialize()
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
continue
initialized_providers[provider_entry_point.name] = provider
try:
for video in videos:
# search for subtitles
subtitles = []
for provider_name, provider in initialized_providers.items():
if provider.check(video):
if provider_name in discarded_providers:
logger.debug('Skipping discarded provider %r', provider_name)
continue
provider_video_languages = provider.languages & languages - video.subtitle_languages
if not provider_video_languages:
logger.debug('Skipping provider %r: no language to search for for video %r', provider_name,
video)
continue
logger.info('Listing subtitles with provider %r for video %r with languages %r',
provider_name, video, provider_video_languages)
try:
provider_subtitles = provider.list_subtitles(video, provider_video_languages)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_name)
discarded_providers.add(provider_name)
continue
except:
logger.exception('Unexpected error in provider %r', provider_name)
continue
logger.info('Found %d subtitles', len(provider_subtitles))
subtitles.extend(provider_subtitles)
# find the best subtitles and download them
downloaded_languages = video.subtitle_languages.copy()
for subtitle, score in sorted([(s, s.compute_score(video)) for s in subtitles],
key=operator.itemgetter(1), reverse=True):
# filter
if subtitle.provider_name in discarded_providers:
logger.debug('Skipping subtitle from discarded provider %r', subtitle.provider_name)
continue
if subtitle.hearing_impaired != hearing_impaired:
logger.debug('Skipping subtitle: hearing impaired != %r', hearing_impaired)
continue
if score < min_score:
logger.debug('Skipping subtitle: score < %d', min_score)
continue
if subtitle.language in downloaded_languages:
logger.debug('Skipping subtitle: %r already downloaded', subtitle.language)
continue
# download
provider = initialized_providers[subtitle.provider_name]
subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language)
logger.info('Downloading subtitle %r with score %d into %r', subtitle, score, subtitle_path)
try:
subtitle_text = provider.download_subtitle(subtitle)
downloaded_subtitles[video].append(subtitle)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', subtitle.provider_name)
discarded_providers.add(subtitle.provider_name)
continue
except InvalidSubtitle:
logger.info('Invalid subtitle, skipping it')
continue
except:
logger.exception('Unexpected error in provider %r', subtitle.provider_name)
continue
with io.open(subtitle_path, 'w', encoding='utf-8') as f:
f.write(subtitle_text)
downloaded_languages.add(subtitle.language)
if single or downloaded_languages >= languages:
logger.debug('All languages downloaded')
break
finally: # terminate providers
for (provider_name, provider) in initialized_providers.items():
try:
provider.terminate()
except ProviderNotAvailable:
logger.warning('Provider %r is not available, unable to terminate', provider_name)
except:
logger.exception('Unexpected error in provider %r', provider_name)
return downloaded_subtitles
-141
View File
@@ -1,141 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from .core import (consume_task, LANGUAGE_INDEX, SERVICE_INDEX,
SERVICE_CONFIDENCE, MATCHING_CONFIDENCE, SERVICES, create_list_tasks,
create_download_tasks, group_by_video, key_subtitles)
from .languages import list_languages
from .tasks import StopTask
import Queue
import logging
import threading
logger = logging.getLogger(__name__)
class Worker(threading.Thread):
"""Consume tasks and put the result in the queue"""
def __init__(self, tasks, results):
super(Worker, self).__init__()
self.tasks = tasks
self.results = results
self.services = {}
def run(self):
while 1:
result = []
try:
task = self.tasks.get(block=True)
if isinstance(task, StopTask):
break
result = consume_task(task, self.services)
self.results.put((task.video, result))
except:
logger.error(u'Exception raised in worker %s' % self.name, exc_info=True)
finally:
self.tasks.task_done()
self.terminate()
logger.debug(u'Thread %s terminated' % self.name)
def terminate(self):
"""Terminate instantiated services"""
for service_name, service in self.services.iteritems():
try:
service.terminate()
except:
logger.error(u'Exception raised when terminating service %s' % service_name, exc_info=True)
class Pool(object):
"""Pool of workers"""
def __init__(self, size):
self.tasks = Queue.Queue()
self.results = Queue.Queue()
self.workers = []
for _ in range(size):
self.workers.append(Worker(self.tasks, self.results))
def __enter__(self):
self.start()
return self
def __exit__(self, *args):
self.stop()
self.join()
def start(self):
"""Start workers"""
for worker in self.workers:
worker.start()
def stop(self):
"""Stop workers"""
for _ in self.workers:
self.tasks.put(StopTask())
def join(self):
"""Join the task queue"""
self.tasks.join()
def collect(self):
"""Collect available results
:return: results of tasks
:rtype: list of :class:`~subliminal.tasks.Task`
"""
results = []
while 1:
try:
result = self.results.get(block=False)
results.append(result)
except Queue.Empty:
break
return results
def list_subtitles(self, paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3):
"""See :meth:`subliminal.list_subtitles`"""
services = services or SERVICES
languages = set(languages or list_languages(1))
if isinstance(paths, basestring):
paths = [paths]
if any([not isinstance(p, unicode) for p in paths]):
logger.warning(u'Not all entries are unicode')
tasks = create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth)
for task in tasks:
self.tasks.put(task)
self.join()
results = self.collect()
return group_by_video(results)
def download_subtitles(self, paths, languages=None, services=None, cache_dir=None, max_depth=3, force=True, multi=False, order=None):
"""See :meth:`subliminal.download_subtitles`"""
services = services or SERVICES
languages = languages or list_languages(1)
if isinstance(paths, basestring):
paths = [paths]
order = order or [LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE]
subtitles_by_video = self.list_subtitles(paths, set(languages), services, force, multi, cache_dir, max_depth)
for video, subtitles in subtitles_by_video.iteritems():
subtitles.sort(key=lambda s: key_subtitles(s, video, languages, services, order), reverse=True)
tasks = create_download_tasks(subtitles_by_video, multi)
for task in tasks:
self.tasks.put(task)
self.join()
results = self.collect()
return results
+6
View File
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
import dogpile.cache
#: The subliminal's dogpile.cache region
region = dogpile.cache.make_region()
+166
View File
@@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
import argparse
import datetime
import logging
import os
import re
import sys
import babelfish
import guessit
import pkg_resources
from subliminal import (__version__, PROVIDERS_ENTRY_POINT, cache_region, Video, Episode, Movie, scan_videos,
download_best_subtitles)
try:
import colorlog
except ImportError:
colorlog = None
DEFAULT_CACHE_FILE = os.path.join('~', '.config', 'subliminal.cache.dbm')
def subliminal():
parser = argparse.ArgumentParser(prog='subliminal', description='Subtitles, faster than your thoughts',
epilog='Suggestions and bug reports are greatly appreciated: '
'https://github.com/Diaoul/subliminal/issues', add_help=False)
# required arguments
required_arguments_group = parser.add_argument_group('required arguments')
required_arguments_group.add_argument('paths', nargs='+', metavar='PATH', help='path to video file or folder')
required_arguments_group.add_argument('-l', '--languages', nargs='+', required=True, metavar='LANGUAGE',
help='wanted languages as alpha2 code (ISO-639-1)')
# configuration
configuration_group = parser.add_argument_group('configuration')
configuration_group.add_argument('-s', '--single', action='store_true',
help='download without language code in subtitle\'s filename i.e. .srt only')
configuration_group.add_argument('-c', '--cache-file', default=DEFAULT_CACHE_FILE,
help='cache file (default: %(default)s)')
# filtering
filtering_group = parser.add_argument_group('filtering')
providers = [ep.name for ep in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT)]
filtering_group.add_argument('-p', '--providers', nargs='+', metavar='PROVIDER',
help='providers to use (%s)' % ', '.join(providers))
filtering_group.add_argument('-m', '--min-score', type=int,
help='minimum score for subtitles (0-%d for episodes, 0-%d for movies)'
% (Episode.scores['hash'], Movie.scores['hash']))
filtering_group.add_argument('-a', '--age', help='download subtitles for videos newer than AGE e.g. 12h, 1w2d')
filtering_group.add_argument('-h', '--hearing-impaired', action='store_true',
help='download hearing impaired subtitles')
filtering_group.add_argument('-f', '--force', action='store_true',
help='force subtitle download for videos with existing subtitles')
# addic7ed
addic7ed_group = parser.add_argument_group('addic7ed')
addic7ed_group.add_argument('--addic7ed-username', metavar='USERNAME', help='username for addic7ed provider')
addic7ed_group.add_argument('--addic7ed-password', metavar='PASSWORD', help='password for addic7ed provider')
# output
output_group = parser.add_argument_group('output')
output_exclusive_group = output_group.add_mutually_exclusive_group()
output_exclusive_group.add_argument('-q', '--quiet', action='store_true', help='disable output')
output_exclusive_group.add_argument('-v', '--verbose', action='store_true', help='verbose output')
output_group.add_argument('--color', action='store_true', help='add color to console output (requires colorlog)')
# troubleshooting
troubleshooting_group = parser.add_argument_group('troubleshooting')
troubleshooting_group.add_argument('--debug', action='store_true', help='debug output')
troubleshooting_group.add_argument('--version', action='version', version=__version__)
troubleshooting_group.add_argument('--help', action='help', help='show this help message and exit')
# parse args
args = parser.parse_args()
# parse paths
try:
args.paths = [os.path.abspath(os.path.expanduser(p.decode('utf-8'))) for p in args.paths]
except UnicodeDecodeError:
parser.error('argument paths: encodings is not utf-8: %r' % args.paths)
# parse languages
try:
args.languages = {babelfish.Language.fromalpha2(l) for l in args.languages}
except babelfish.Error:
parser.error('argument -l/--languages: codes are not ISO-639-1: %r' % args.languages)
# parse age
if args.age is not None:
match = re.match(r'^(?:(?P<weeks>\d+?)w)?(?:(?P<days>\d+?)d)?(?:(?P<hours>\d+?)h)?$', args.age)
if not match:
parser.error('argument -a/--age: invalid age: %r' % args.age)
args.age = datetime.timedelta(**{k: int(v) for k, v in match.groupdict(0).items()})
# parse cache-file
args.cache_file = os.path.abspath(os.path.expanduser(args.cache_file))
if not os.path.exists(os.path.split(args.cache_file)[0]):
parser.error('argument -c/--cache-file: directory %r for cache file does not exist'
% os.path.split(args.cache_file)[0])
# parse provider configs
provider_configs = {}
if (args.addic7ed_username is not None and args.addic7ed_password is None
or args.addic7ed_username is None and args.addic7ed_password is not None):
parser.error('argument --addic7ed-username/--addic7ed-password: both arguments are required or none')
if args.addic7ed_username is not None and args.addic7ed_password is not None:
provider_configs['addic7ed'] = {'username': args.addic7ed_username, 'password': args.addic7ed_password}
# parse color
if args.color and colorlog is None:
parser.error('argument --color: colorlog required')
# setup output
if args.debug:
handler = logging.StreamHandler()
if args.color:
handler.setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)-8s%(reset)s [%(blue)s%(name)s-%(funcName)s:%(lineno)d%(reset)s] %(message)s',
log_colors=dict(colorlog.default_log_colors.items() + [('DEBUG', 'cyan')])))
else:
handler.setFormatter(logging.Formatter('%(levelname)-8s [%(name)s-%(funcName)s:%(lineno)d] %(message)s'))
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.DEBUG)
elif args.verbose:
handler = logging.StreamHandler()
if args.color:
handler.setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)-8s%(reset)s [%(blue)s%(name)s%(reset)s] %(message)s'))
else:
handler.setFormatter(logging.Formatter('%(levelname)-8s [%(name)s] %(message)s'))
logging.getLogger('subliminal').addHandler(handler)
logging.getLogger('subliminal').setLevel(logging.INFO)
elif not args.quiet:
handler = logging.StreamHandler()
if args.color:
handler.setFormatter(colorlog.ColoredFormatter('[%(log_color)s%(levelname)s%(reset)s] %(message)s'))
else:
handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
logging.getLogger('subliminal.api').addHandler(handler)
logging.getLogger('subliminal.api').setLevel(logging.INFO)
# configure cache
cache_region.configure('dogpile.cache.dbm', arguments={'filename': args.cache_file})
# scan videos
videos = scan_videos([p for p in args.paths if os.path.exists(p)], subtitles=not args.force,
embedded_subtitles=not args.force, age=args.age)
# guess videos
videos.extend([Video.fromguess(os.path.split(p)[1], guessit.guess_file_info(p, 'autodetect')) for p in args.paths
if not os.path.exists(p)])
# download best subtitles
subtitles = download_best_subtitles(videos, args.languages, providers=args.providers,
provider_configs=provider_configs, single=args.single,
min_score=args.min_score, hearing_impaired=args.hearing_impaired)
# result output
if not subtitles:
if not args.quiet:
sys.stderr.write('No subtitles downloaded\n')
exit(1)
if not args.quiet:
subtitles_count = sum([len(s) for s in subtitles.values()])
if subtitles_count == 1:
print('%d subtitle downloaded' % subtitles_count)
else:
print('%d subtitles downloaded' % subtitles_count)
View File
+29
View File
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from babelfish.converters.name import NameConverter
class Addic7edConverter(NameConverter):
def __init__(self):
super(Addic7edConverter, self).__init__()
self.from_addic7ed = {'Català': ('cat', None), 'Chinese (Simplified)': ('zho', None),
'Chinese (Traditional)': ('zho', None), 'Euskera': ('eus', None),
'Galego': ('glg', None), 'Greek': ('ell', None),
'Malay': ('msa', None), 'Portuguese (Brazilian)': ('por', 'BR'),
'Serbian (Cyrillic)': ('srp', None), 'Serbian (Latin)': ('srp', None),
'Spanish (Latin America)': ('spa', None), 'Spanish (Spain)': ('spa', None)}
self.to_addic7ed = {('cat', None): 'Català', ('zho', None): 'Chinese (Simplified)',
('eus', None): 'Euskera', ('glg', None): 'Galego',
('ell', None): 'Greek', ('msa', None): 'Malay',
('por', 'BR'): 'Portuguese (Brazilian)', ('srp', None): 'Serbian (Cyrillic)'}
self.codes |= set(self.from_addic7ed.keys())
def convert(self, alpha3, country=None):
if (alpha3, country) in self.to_addic7ed:
return self.to_addic7ed[(alpha3, country)]
return super(Addic7edConverter, self).convert(alpha3, country)
def reverse(self, addic7ed):
if addic7ed in self.from_addic7ed:
return self.from_addic7ed[addic7ed]
return super(Addic7edConverter, self).reverse(addic7ed)
+22
View File
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from babelfish.converters.alpha2 import Alpha2Converter
class TVsubtitlesConverter(Alpha2Converter):
def __init__(self):
super(TVsubtitlesConverter, self).__init__()
self.from_tvsubtitles = {'br': ('por', 'BR'), 'ua': ('ukr', None), 'gr': ('ell', None), 'cn': ('zho', None),
'jp': ('jpn', None), 'cz': ('ces', None)}
self.to_tvsubtitles = {v: k for k, v in self.from_tvsubtitles}
self.codes |= set(self.from_tvsubtitles.keys())
def convert(self, alpha3, country=None):
if (alpha3, country) in self.to_tvsubtitles:
return self.to_tvsubtitles[(alpha3, country)]
return super(TVsubtitlesConverter, self).convert(alpha3, country)
def reverse(self, tvsubtitles):
if tvsubtitles in self.from_tvsubtitles:
return self.from_tvsubtitles[tvsubtitles]
return super(TVsubtitlesConverter, self).reverse(tvsubtitles)
-240
View File
@@ -1,240 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from .exceptions import DownloadFailedError
from .services import ServiceConfig
from .tasks import DownloadTask, ListTask
from .utils import get_keywords
from .videos import Episode, Movie, scan
from collections import defaultdict
from itertools import groupby
import guessit
import logging
__all__ = ['SERVICES', 'LANGUAGE_INDEX', 'SERVICE_INDEX', 'SERVICE_CONFIDENCE', 'MATCHING_CONFIDENCE',
'create_list_tasks', 'create_download_tasks', 'consume_task', 'matching_confidence',
'key_subtitles', 'group_by_video']
logger = logging.getLogger(__name__)
SERVICES = ['opensubtitles', 'bierdopje', 'subswiki', 'subtitulos', 'thesubdb']
LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE = range(4)
def create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth):
"""Create a list of :class:`~subliminal.tasks.ListTask` from one or more paths using the given criteria
:param paths: path(s) to video file or folder
:type paths: string or list
:param set languages: languages to search for
:param list services: services to use for the search
:param bool force: force searching for subtitles even if some are detected
:param bool multi: search multiple languages for the same video
:param string cache_dir: path to the cache directory to use
:param int max_depth: maximum depth for scanning entries
:return: the created tasks
:rtype: list of :class:`~subliminal.tasks.ListTask`
"""
scan_result = []
for p in paths:
scan_result.extend(scan(p, max_depth))
logger.debug(u'Found %d videos in %r with maximum depth %d' % (len(scan_result), paths, max_depth))
tasks = []
config = ServiceConfig(multi, cache_dir)
for video, detected_subtitles in scan_result:
detected_languages = set([s.language for s in detected_subtitles])
wanted_languages = languages.copy()
if not force and multi:
wanted_languages -= detected_languages
if not wanted_languages:
logger.debug(u'No need to list multi subtitles %r for %r because %r detected' % (languages, video, detected_languages))
continue
if not force and not multi and None in detected_languages:
logger.debug(u'No need to list single subtitles %r for %r because one detected' % (languages, video))
continue
logger.debug(u'Listing subtitles %r for %r with services %r' % (wanted_languages, video, services))
for service_name in services:
mod = __import__('services.' + service_name, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
service = mod.Service
service_languages = wanted_languages & service.available_languages()
if not service_languages:
logger.debug(u'Skipping %r: none of wanted languages %r available for service %s' % (video, wanted_languages, service_name))
continue
if not service.is_valid_video(video):
logger.debug(u'Skipping %r: not part of supported videos %r for service %s' % (video, service.videos, service_name))
continue
task = ListTask(video, service_languages, service_name, config)
logger.debug(u'Created task %r' % task)
tasks.append(task)
return tasks
def create_download_tasks(subtitles_by_video, multi):
"""Create a list of :class:`~subliminal.tasks.DownloadTask` from a list results grouped by video
:param subtitles_by_video: :class:`~subliminal.tasks.ListTask` results grouped by video and sorted
:type subtitles_by_video: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.Subtitle`]
:param order: preferred order for subtitles sorting
:type list: list of :data:`LANGUAGE_INDEX`, :data:`SERVICE_INDEX`, :data:`SERVICE_CONFIDENCE`, :data:`MATCHING_CONFIDENCE`
:param bool multi: download multiple languages for the same video
:return: the created tasks
:rtype: list of :class:`~subliminal.tasks.DownloadTask`
"""
tasks = []
for video, subtitles in subtitles_by_video.iteritems():
if not subtitles:
continue
if not multi:
task = DownloadTask(video, list(subtitles))
logger.debug(u'Created task %r' % task)
tasks.append(task)
continue
for _, by_language in groupby(subtitles, lambda s: s.language):
task = DownloadTask(video, list(by_language))
logger.debug(u'Created task %r' % task)
tasks.append(task)
return tasks
def consume_task(task, services=None):
"""Consume a task. If the ``services`` parameter is given, the function will attempt
to get the service from it. In case the service is not in ``services``, it will be initialized
and put in ``services``
:param task: task to consume
:type task: :class:`~subliminal.tasks.ListTask` or :class:`~subliminal.tasks.DownloadTask`
:param dict services: mapping between the service name and an instance of this service
:return: the result of the task
:rtype: list of :class:`~subliminal.subtitles.ResultSubtitle` or :class:`~subliminal.subtitles.Subtitle`
"""
if services is None:
services = {}
logger.info(u'Consuming %r' % task)
result = None
if isinstance(task, ListTask):
if task.service not in services:
mod = __import__('services.' + task.service, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
services[task.service] = mod.Service(task.config)
services[task.service].init()
subtitles = services[task.service].list(task.video, task.languages)
result = subtitles
elif isinstance(task, DownloadTask):
for subtitle in task.subtitles:
if subtitle.service not in services:
mod = __import__('services.' + subtitle.service, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
services[subtitle.service] = mod.Service()
services[subtitle.service].init()
try:
services[subtitle.service].download(subtitle)
result = subtitle
break
except DownloadFailedError:
logger.warning(u'Could not download subtitle %r, trying next' % subtitle)
continue
if result is None:
logger.error(u'No subtitles could be downloaded for video %r' % task.video)
return result
def matching_confidence(video, subtitle):
"""Compute the probability (confidence) that the subtitle matches the video
:param video: video to match
:type video: :class:`~subliminal.videos.Video`
:param subtitle: subtitle to match
:type subtitle: :class:`~subliminal.subtitles.Subtitle`
:return: the matching probability
:rtype: float
"""
guess = guessit.guess_file_info(subtitle.release, 'autodetect')
video_keywords = get_keywords(video.guess)
subtitle_keywords = get_keywords(guess) | subtitle.keywords
replacement = {'keywords': len(video_keywords & subtitle_keywords)}
if isinstance(video, Episode):
replacement.update({'series': 0, 'season': 0, 'episode': 0})
matching_format = '{series:b}{season:b}{episode:b}{keywords:03b}'
best = matching_format.format(series=1, season=1, episode=1, keywords=len(video_keywords))
if guess['type'] in ['episode', 'episodesubtitle']:
if 'series' in guess and guess['series'].lower() == video.series.lower():
replacement['series'] = 1
if 'season' in guess and guess['season'] == video.season:
replacement['season'] = 1
if 'episodeNumber' in guess and guess['episodeNumber'] == video.episode:
replacement['episode'] = 1
elif isinstance(video, Movie):
replacement.update({'title': 0, 'year': 0})
matching_format = '{title:b}{year:b}{keywords:03b}'
best = matching_format.format(title=1, year=1, keywords=len(video_keywords))
if guess['type'] in ['movie', 'moviesubtitle']:
if 'title' in guess and guess['title'].lower() == video.title.lower():
replacement['title'] = 1
if 'year' in guess and guess['year'] == video.year:
replacement['year'] = 1
else:
return 0
confidence = float(int(matching_format.format(**replacement), 2)) / float(int(best, 2))
return confidence
def key_subtitles(subtitle, video, languages, services, order):
"""Create a key to sort subtitle using the given order
:param subtitle: subtitle to sort
:type subtitle: :class:`~subliminal.subtitles.ResultSubtitle`
:param video: video to match
:type video: :class:`~subliminal.videos.Video`
:param list languages: languages in preferred order
:param list services: services in preferred order
:param order: preferred order for subtitles sorting
:type list: list of :data:`LANGUAGE_INDEX`, :data:`SERVICE_INDEX`, :data:`SERVICE_CONFIDENCE`, :data:`MATCHING_CONFIDENCE`
:return: a key ready to use for subtitles sorting
:rtype: int
"""
key = ''
for sort_item in order:
if sort_item == LANGUAGE_INDEX:
key += '{0:03d}'.format(len(languages) - languages.index(subtitle.language) - 1)
elif sort_item == SERVICE_INDEX:
key += '{0:02d}'.format(len(services) - services.index(subtitle.service) - 1)
elif sort_item == SERVICE_CONFIDENCE:
key += '{0:04d}'.format(int(subtitle.confidence * 1000))
elif sort_item == MATCHING_CONFIDENCE:
confidence = 0
if subtitle.release:
confidence = matching_confidence(video, subtitle)
key += '{0:04d}'.format(int(confidence * 1000))
return int(key)
def group_by_video(list_results):
"""Group the results of :class:`ListTasks <subliminal.tasks.ListTask>` into a
dictionary of :class:`~subliminal.videos.Video` => :class:`~subliminal.subtitles.Subtitle`
:param list_results:
:type list_results: list of result of :class:`~subliminal.tasks.ListTask`
:return: subtitles grouped by videos
:rtype: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.Subtitle`]
"""
result = defaultdict(list)
for video, subtitles in list_results:
result[video] += subtitles
return result
+9 -63
View File
@@ -1,20 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
class Error(Exception):
@@ -22,60 +7,21 @@ class Error(Exception):
pass
class InvalidLanguageError(Error):
"""Exception raised when invalid language is submitted
Attributes:
language -- language that cause the error
"""
def __init__(self, language):
self.language = language
def __str__(self):
return self.language
class MissingLanguageError(Error):
"""Exception raised when a missing language is found
Attributes:
language -- the missing language
"""
def __init__(self, language):
self.language = language
def __str__(self):
return self.language
class InvalidServiceError(Error):
"""Exception raised when invalid service is submitted
:param string service: service that causes the error
"""
def __init__(self, service):
self.service = service
def __str__(self):
return self.service
class ServiceError(Error):
""""Exception raised by services"""
class ProviderError(Error):
"""Exception raised by providers"""
pass
class WrongTaskError(Error):
""""Exception raised when invalid task is submitted"""
class ProviderConfigurationError(ProviderError):
"""Exception raised by providers when badly configured"""
pass
class DownloadFailedError(Error):
""""Exception raised when a download task has failed in service"""
class ProviderNotAvailable(ProviderError):
"""Exception raised by providers when unavailable"""
pass
class UnknownVideoError(Error):
""""Exception raised when a video could not be identified"""
class InvalidSubtitle(ProviderError):
"""Exception raised by providers when the downloaded subtitle is invalid"""
pass
-18
View File
@@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
__version__ = '0.5.1'
-547
View File
@@ -1,547 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
__all__ = ['convert_language', 'list_languages', 'LANGUAGES']
def convert_language(language, to_iso, from_iso=None):
"""Convert a language into another format
:param string language: language
:param int to_iso: convert language to ISO-639-x
:param int from_iso: convert language from ISO-639-x
:return: converted language
:rtype: string
"""
if from_iso == None: # if no from_iso is given, try to guess it
if language.startswith(language[:1].upper()):
from_iso = 0
elif len(language) == 2:
from_iso = 1
elif len(language) == 3:
from_iso = 2
else:
raise ValueError('Invalid input language format')
if isinstance(language, unicode):
language = language.encode('utf-8')
converted_language = None
for language_tuple in LANGUAGES:
if language_tuple[from_iso] == language and language_tuple[to_iso]:
converted_language = language_tuple[to_iso]
break
return converted_language
def list_languages(iso):
"""List languages in the given ISO-639-x format
:param int iso: ISO-639-x format to list
:return: languages in the requested format
:rtype: list
"""
return [l[iso] for l in LANGUAGES if l[iso]]
#: ISO-639-2 languages list from http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
#: + ('Brazilian', 'po', 'pob')
LANGUAGES = [('Afar', 'aa', 'aar'),
('Abkhazian', 'ab', 'abk'),
('Achinese', '', 'ace'),
('Acoli', '', 'ach'),
('Adangme', '', 'ada'),
('Adyghe; Adygei', '', 'ady'),
('Afro-Asiatic languages', '', 'afa'),
('Afrihili', '', 'afh'),
('Afrikaans', 'af', 'afr'),
('Ainu', '', 'ain'),
('Akan', 'ak', 'aka'),
('Akkadian', '', 'akk'),
('Albanian', 'sq', 'alb'),
('Aleut', '', 'ale'),
('Algonquian languages', '', 'alg'),
('Southern Altai', '', 'alt'),
('Amharic', 'am', 'amh'),
('English, Old (ca.450-1100)', '', 'ang'),
('Angika', '', 'anp'),
('Apache languages', '', 'apa'),
('Arabic', 'ar', 'ara'),
('Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)', '', 'arc'),
('Aragonese', 'an', 'arg'),
('Armenian', 'hy', 'arm'),
('Mapudungun; Mapuche', '', 'arn'),
('Arapaho', '', 'arp'),
('Artificial languages', '', 'art'),
('Arawak', '', 'arw'),
('Assamese', 'as', 'asm'),
('Asturian; Bable; Leonese; Asturleonese', '', 'ast'),
('Athapascan languages', '', 'ath'),
('Australian languages', '', 'aus'),
('Avaric', 'av', 'ava'),
('Avestan', 'ae', 'ave'),
('Awadhi', '', 'awa'),
('Aymara', 'ay', 'aym'),
('Azerbaijani', 'az', 'aze'),
('Banda languages', '', 'bad'),
('Bamileke languages', '', 'bai'),
('Bashkir', 'ba', 'bak'),
('Baluchi', '', 'bal'),
('Bambara', 'bm', 'bam'),
('Balinese', '', 'ban'),
('Basque', 'eu', 'baq'),
('Basa', '', 'bas'),
('Baltic languages', '', 'bat'),
('Beja; Bedawiyet', '', 'bej'),
('Belarusian', 'be', 'bel'),
('Bemba', '', 'bem'),
('Bengali', 'bn', 'ben'),
('Berber languages', '', 'ber'),
('Bhojpuri', '', 'bho'),
('Bihari languages', 'bh', 'bih'),
('Bikol', '', 'bik'),
('Bini; Edo', '', 'bin'),
('Bislama', 'bi', 'bis'),
('Siksika', '', 'bla'),
('Bantu (Other)', '', 'bnt'),
('Bosnian', 'bs', 'bos'),
('Braj', '', 'bra'),
('Breton', 'br', 'bre'),
('Batak languages', '', 'btk'),
('Buriat', '', 'bua'),
('Buginese', '', 'bug'),
('Bulgarian', 'bg', 'bul'),
('Burmese', 'my', 'bur'),
('Blin; Bilin', '', 'byn'),
('Caddo', '', 'cad'),
('Central American Indian languages', '', 'cai'),
('Galibi Carib', '', 'car'),
('Catalan; Valencian', 'ca', 'cat'),
('Caucasian languages', '', 'cau'),
('Cebuano', '', 'ceb'),
('Celtic languages', '', 'cel'),
('Chamorro', 'ch', 'cha'),
('Chibcha', '', 'chb'),
('Chechen', 'ce', 'che'),
('Chagatai', '', 'chg'),
('Chinese', 'zh', 'chi'),
('Chuukese', '', 'chk'),
('Mari', '', 'chm'),
('Chinook jargon', '', 'chn'),
('Choctaw', '', 'cho'),
('Chipewyan; Dene Suline', '', 'chp'),
('Cherokee', '', 'chr'),
('Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic', 'cu', 'chu'),
('Chuvash', 'cv', 'chv'),
('Cheyenne', '', 'chy'),
('Chamic languages', '', 'cmc'),
('Coptic', '', 'cop'),
('Cornish', 'kw', 'cor'),
('Corsican', 'co', 'cos'),
('Creoles and pidgins, English based', '', 'cpe'),
('Creoles and pidgins, French-based ', '', 'cpf'),
('Creoles and pidgins, Portuguese-based ', '', 'cpp'),
('Cree', 'cr', 'cre'),
('Crimean Tatar; Crimean Turkish', '', 'crh'),
('Creoles and pidgins ', '', 'crp'),
('Kashubian', '', 'csb'),
('Cushitic languages', '', 'cus'),
('Czech', 'cs', 'cze'),
('Dakota', '', 'dak'),
('Danish', 'da', 'dan'),
('Dargwa', '', 'dar'),
('Land Dayak languages', '', 'day'),
('Delaware', '', 'del'),
('Slave (Athapascan)', '', 'den'),
('Dogrib', '', 'dgr'),
('Dinka', '', 'din'),
('Divehi; Dhivehi; Maldivian', 'dv', 'div'),
('Dogri', '', 'doi'),
('Dravidian languages', '', 'dra'),
('Lower Sorbian', '', 'dsb'),
('Duala', '', 'dua'),
('Dutch, Middle (ca.1050-1350)', '', 'dum'),
('Dutch; Flemish', 'nl', 'dut'),
('Dyula', '', 'dyu'),
('Dzongkha', 'dz', 'dzo'),
('Efik', '', 'efi'),
('Egyptian (Ancient)', '', 'egy'),
('Ekajuk', '', 'eka'),
('Elamite', '', 'elx'),
('English', 'en', 'eng'),
('English, Middle (1100-1500)', '', 'enm'),
('Esperanto', 'eo', 'epo'),
('Estonian', 'et', 'est'),
('Ewe', 'ee', 'ewe'),
('Ewondo', '', 'ewo'),
('Fang', '', 'fan'),
('Faroese', 'fo', 'fao'),
('Fanti', '', 'fat'),
('Fijian', 'fj', 'fij'),
('Filipino; Pilipino', '', 'fil'),
('Finnish', 'fi', 'fin'),
('Finno-Ugrian languages', '', 'fiu'),
('Fon', '', 'fon'),
('French', 'fr', 'fre'),
('French, Middle (ca.1400-1600)', '', 'frm'),
('French, Old (842-ca.1400)', '', 'fro'),
('Northern Frisian', '', 'frr'),
('Eastern Frisian', '', 'frs'),
('Western Frisian', 'fy', 'fry'),
('Fulah', 'ff', 'ful'),
('Friulian', '', 'fur'),
('Ga', '', 'gaa'),
('Gayo', '', 'gay'),
('Gbaya', '', 'gba'),
('Germanic languages', '', 'gem'),
('Georgian', 'ka', 'geo'),
('German', 'de', 'ger'),
('Geez', '', 'gez'),
('Gilbertese', '', 'gil'),
('Gaelic; Scottish Gaelic', 'gd', 'gla'),
('Irish', 'ga', 'gle'),
('Galician', 'gl', 'glg'),
('Manx', 'gv', 'glv'),
('German, Middle High (ca.1050-1500)', '', 'gmh'),
('German, Old High (ca.750-1050)', '', 'goh'),
('Gondi', '', 'gon'),
('Gorontalo', '', 'gor'),
('Gothic', '', 'got'),
('Grebo', '', 'grb'),
('Greek, Ancient (to 1453)', '', 'grc'),
('Greek, Modern (1453-)', 'el', 'gre'),
('Guarani', 'gn', 'grn'),
('Swiss German; Alemannic; Alsatian', '', 'gsw'),
('Gujarati', 'gu', 'guj'),
('Gwich\'in', '', 'gwi'),
('Haida', '', 'hai'),
('Haitian; Haitian Creole', 'ht', 'hat'),
('Hausa', 'ha', 'hau'),
('Hawaiian', '', 'haw'),
('Hebrew', 'he', 'heb'),
('Herero', 'hz', 'her'),
('Hiligaynon', '', 'hil'),
('Himachali languages; Western Pahari languages', '', 'him'),
('Hindi', 'hi', 'hin'),
('Hittite', '', 'hit'),
('Hmong; Mong', '', 'hmn'),
('Hiri Motu', 'ho', 'hmo'),
('Croatian', 'hr', 'hrv'),
('Upper Sorbian', '', 'hsb'),
('Hungarian', 'hu', 'hun'),
('Hupa', '', 'hup'),
('Iban', '', 'iba'),
('Igbo', 'ig', 'ibo'),
('Icelandic', 'is', 'ice'),
('Ido', 'io', 'ido'),
('Sichuan Yi; Nuosu', 'ii', 'iii'),
('Ijo languages', '', 'ijo'),
('Inuktitut', 'iu', 'iku'),
('Interlingue; Occidental', 'ie', 'ile'),
('Iloko', '', 'ilo'),
('Interlingua (International Auxiliary Language Association)', 'ia', 'ina'),
('Indic languages', '', 'inc'),
('Indonesian', 'id', 'ind'),
('Indo-European languages', '', 'ine'),
('Ingush', '', 'inh'),
('Inupiaq', 'ik', 'ipk'),
('Iranian languages', '', 'ira'),
('Iroquoian languages', '', 'iro'),
('Italian', 'it', 'ita'),
('Javanese', 'jv', 'jav'),
('Lojban', '', 'jbo'),
('Japanese', 'ja', 'jpn'),
('Judeo-Persian', '', 'jpr'),
('Judeo-Arabic', '', 'jrb'),
('Kara-Kalpak', '', 'kaa'),
('Kabyle', '', 'kab'),
('Kachin; Jingpho', '', 'kac'),
('Kalaallisut; Greenlandic', 'kl', 'kal'),
('Kamba', '', 'kam'),
('Kannada', 'kn', 'kan'),
('Karen languages', '', 'kar'),
('Kashmiri', 'ks', 'kas'),
('Kanuri', 'kr', 'kau'),
('Kawi', '', 'kaw'),
('Kazakh', 'kk', 'kaz'),
('Kabardian', '', 'kbd'),
('Khasi', '', 'kha'),
('Khoisan languages', '', 'khi'),
('Central Khmer', 'km', 'khm'),
('Khotanese; Sakan', '', 'kho'),
('Kikuyu; Gikuyu', 'ki', 'kik'),
('Kinyarwanda', 'rw', 'kin'),
('Kirghiz; Kyrgyz', 'ky', 'kir'),
('Kimbundu', '', 'kmb'),
('Konkani', '', 'kok'),
('Komi', 'kv', 'kom'),
('Kongo', 'kg', 'kon'),
('Korean', 'ko', 'kor'),
('Kosraean', '', 'kos'),
('Kpelle', '', 'kpe'),
('Karachay-Balkar', '', 'krc'),
('Karelian', '', 'krl'),
('Kru languages', '', 'kro'),
('Kurukh', '', 'kru'),
('Kuanyama; Kwanyama', 'kj', 'kua'),
('Kumyk', '', 'kum'),
('Kurdish', 'ku', 'kur'),
('Kutenai', '', 'kut'),
('Ladino', '', 'lad'),
('Lahnda', '', 'lah'),
('Lamba', '', 'lam'),
('Lao', 'lo', 'lao'),
('Latin', 'la', 'lat'),
('Latvian', 'lv', 'lav'),
('Lezghian', '', 'lez'),
('Limburgan; Limburger; Limburgish', 'li', 'lim'),
('Lingala', 'ln', 'lin'),
('Lithuanian', 'lt', 'lit'),
('Mongo', '', 'lol'),
('Lozi', '', 'loz'),
('Luxembourgish; Letzeburgesch', 'lb', 'ltz'),
('Luba-Lulua', '', 'lua'),
('Luba-Katanga', 'lu', 'lub'),
('Ganda', 'lg', 'lug'),
('Luiseno', '', 'lui'),
('Lunda', '', 'lun'),
('Luo (Kenya and Tanzania)', '', 'luo'),
('Lushai', '', 'lus'),
('Macedonian', 'mk', 'mac'),
('Madurese', '', 'mad'),
('Magahi', '', 'mag'),
('Marshallese', 'mh', 'mah'),
('Maithili', '', 'mai'),
('Makasar', '', 'mak'),
('Malayalam', 'ml', 'mal'),
('Mandingo', '', 'man'),
('Maori', 'mi', 'mao'),
('Austronesian languages', '', 'map'),
('Marathi', 'mr', 'mar'),
('Masai', '', 'mas'),
('Malay', 'ms', 'may'),
('Moksha', '', 'mdf'),
('Mandar', '', 'mdr'),
('Mende', '', 'men'),
('Irish, Middle (900-1200)', '', 'mga'),
('Mi\'kmaq; Micmac', '', 'mic'),
('Minangkabau', '', 'min'),
('Uncoded languages', '', 'mis'),
('Mon-Khmer languages', '', 'mkh'),
('Malagasy', 'mg', 'mlg'),
('Maltese', 'mt', 'mlt'),
('Manchu', '', 'mnc'),
('Manipuri', '', 'mni'),
('Manobo languages', '', 'mno'),
('Mohawk', '', 'moh'),
('Mongolian', 'mn', 'mon'),
('Mossi', '', 'mos'),
('Multiple languages', '', 'mul'),
('Munda languages', '', 'mun'),
('Creek', '', 'mus'),
('Mirandese', '', 'mwl'),
('Marwari', '', 'mwr'),
('Mayan languages', '', 'myn'),
('Erzya', '', 'myv'),
('Nahuatl languages', '', 'nah'),
('North American Indian languages', '', 'nai'),
('Neapolitan', '', 'nap'),
('Nauru', 'na', 'nau'),
('Navajo; Navaho', 'nv', 'nav'),
('Ndebele, South; South Ndebele', 'nr', 'nbl'),
('Ndebele, North; North Ndebele', 'nd', 'nde'),
('Ndonga', 'ng', 'ndo'),
('Low German; Low Saxon; German, Low; Saxon, Low', '', 'nds'),
('Nepali', 'ne', 'nep'),
('Nepal Bhasa; Newari', '', 'new'),
('Nias', '', 'nia'),
('Niger-Kordofanian languages', '', 'nic'),
('Niuean', '', 'niu'),
('Norwegian Nynorsk; Nynorsk, Norwegian', 'nn', 'nno'),
('Bokmål, Norwegian; Norwegian Bokmål', 'nb', 'nob'),
('Nogai', '', 'nog'),
('Norse, Old', '', 'non'),
('Norwegian', 'no', 'nor'),
('N\'Ko', '', 'nqo'),
('Pedi; Sepedi; Northern Sotho', '', 'nso'),
('Nubian languages', '', 'nub'),
('Classical Newari; Old Newari; Classical Nepal Bhasa', '', 'nwc'),
('Chichewa; Chewa; Nyanja', 'ny', 'nya'),
('Nyamwezi', '', 'nym'),
('Nyankole', '', 'nyn'),
('Nyoro', '', 'nyo'),
('Nzima', '', 'nzi'),
('Occitan (post 1500); Provençal', 'oc', 'oci'),
('Ojibwa', 'oj', 'oji'),
('Oriya', 'or', 'ori'),
('Oromo', 'om', 'orm'),
('Osage', '', 'osa'),
('Ossetian; Ossetic', 'os', 'oss'),
('Turkish, Ottoman (1500-1928)', '', 'ota'),
('Otomian languages', '', 'oto'),
('Papuan languages', '', 'paa'),
('Pangasinan', '', 'pag'),
('Pahlavi', '', 'pal'),
('Pampanga; Kapampangan', '', 'pam'),
('Panjabi; Punjabi', 'pa', 'pan'),
('Papiamento', '', 'pap'),
('Palauan', '', 'pau'),
('Persian, Old (ca.600-400 B.C.)', '', 'peo'),
('Persian', 'fa', 'per'),
('Philippine languages', '', 'phi'),
('Phoenician', '', 'phn'),
('Pali', 'pi', 'pli'),
('Polish', 'pl', 'pol'),
('Pohnpeian', '', 'pon'),
('Portuguese', 'pt', 'por'),
('Prakrit languages', '', 'pra'),
('Provençal, Old (to 1500)', '', 'pro'),
('Pushto; Pashto', 'ps', 'pus'),
('Reserved for local use', '', 'qaa-qtz'),
('Quechua', 'qu', 'que'),
('Rajasthani', '', 'raj'),
('Rapanui', '', 'rap'),
('Rarotongan; Cook Islands Maori', '', 'rar'),
('Romance languages', '', 'roa'),
('Romansh', 'rm', 'roh'),
('Romany', '', 'rom'),
('Romanian; Moldavian; Moldovan', 'ro', 'rum'),
('Rundi', 'rn', 'run'),
('Aromanian; Arumanian; Macedo-Romanian', '', 'rup'),
('Russian', 'ru', 'rus'),
('Sandawe', '', 'sad'),
('Sango', 'sg', 'sag'),
('Yakut', '', 'sah'),
('South American Indian (Other)', '', 'sai'),
('Salishan languages', '', 'sal'),
('Samaritan Aramaic', '', 'sam'),
('Sanskrit', 'sa', 'san'),
('Sasak', '', 'sas'),
('Santali', '', 'sat'),
('Sicilian', '', 'scn'),
('Scots', '', 'sco'),
('Selkup', '', 'sel'),
('Semitic languages', '', 'sem'),
('Irish, Old (to 900)', '', 'sga'),
('Sign Languages', '', 'sgn'),
('Shan', '', 'shn'),
('Sidamo', '', 'sid'),
('Sinhala; Sinhalese', 'si', 'sin'),
('Siouan languages', '', 'sio'),
('Sino-Tibetan languages', '', 'sit'),
('Slavic languages', '', 'sla'),
('Slovak', 'sk', 'slo'),
('Slovenian', 'sl', 'slv'),
('Southern Sami', '', 'sma'),
('Northern Sami', 'se', 'sme'),
('Sami languages', '', 'smi'),
('Lule Sami', '', 'smj'),
('Inari Sami', '', 'smn'),
('Samoan', 'sm', 'smo'),
('Skolt Sami', '', 'sms'),
('Shona', 'sn', 'sna'),
('Sindhi', 'sd', 'snd'),
('Soninke', '', 'snk'),
('Sogdian', '', 'sog'),
('Somali', 'so', 'som'),
('Songhai languages', '', 'son'),
('Sotho, Southern', 'st', 'sot'),
('Spanish; Castilian', 'es', 'spa'),
('Sardinian', 'sc', 'srd'),
('Sranan Tongo', '', 'srn'),
('Serbian', 'sr', 'srp'),
('Serer', '', 'srr'),
('Nilo-Saharan languages', '', 'ssa'),
('Swati', 'ss', 'ssw'),
('Sukuma', '', 'suk'),
('Sundanese', 'su', 'sun'),
('Susu', '', 'sus'),
('Sumerian', '', 'sux'),
('Swahili', 'sw', 'swa'),
('Swedish', 'sv', 'swe'),
('Classical Syriac', '', 'syc'),
('Syriac', '', 'syr'),
('Tahitian', 'ty', 'tah'),
('Tai languages', '', 'tai'),
('Tamil', 'ta', 'tam'),
('Tatar', 'tt', 'tat'),
('Telugu', 'te', 'tel'),
('Timne', '', 'tem'),
('Tereno', '', 'ter'),
('Tetum', '', 'tet'),
('Tajik', 'tg', 'tgk'),
('Tagalog', 'tl', 'tgl'),
('Thai', 'th', 'tha'),
('Tibetan', 'bo', 'tib'),
('Tigre', '', 'tig'),
('Tigrinya', 'ti', 'tir'),
('Tiv', '', 'tiv'),
('Tokelau', '', 'tkl'),
('Klingon; tlhIngan-Hol', '', 'tlh'),
('Tlingit', '', 'tli'),
('Tamashek', '', 'tmh'),
('Tonga (Nyasa)', '', 'tog'),
('Tonga (Tonga Islands)', 'to', 'ton'),
('Tok Pisin', '', 'tpi'),
('Tsimshian', '', 'tsi'),
('Tswana', 'tn', 'tsn'),
('Tsonga', 'ts', 'tso'),
('Turkmen', 'tk', 'tuk'),
('Tumbuka', '', 'tum'),
('Tupi languages', '', 'tup'),
('Turkish', 'tr', 'tur'),
('Altaic languages', '', 'tut'),
('Tuvalu', '', 'tvl'),
('Twi', 'tw', 'twi'),
('Tuvinian', '', 'tyv'),
('Udmurt', '', 'udm'),
('Ugaritic', '', 'uga'),
('Uighur; Uyghur', 'ug', 'uig'),
('Ukrainian', 'uk', 'ukr'),
('Umbundu', '', 'umb'),
('Undetermined', '', 'und'),
('Urdu', 'ur', 'urd'),
('Uzbek', 'uz', 'uzb'),
('Vai', '', 'vai'),
('Venda', 've', 'ven'),
('Vietnamese', 'vi', 'vie'),
('Volapük', 'vo', 'vol'),
('Votic', '', 'vot'),
('Wakashan languages', '', 'wak'),
('Walamo', '', 'wal'),
('Waray', '', 'war'),
('Washo', '', 'was'),
('Welsh', 'cy', 'wel'),
('Sorbian languages', '', 'wen'),
('Walloon', 'wa', 'wln'),
('Wolof', 'wo', 'wol'),
('Kalmyk; Oirat', '', 'xal'),
('Xhosa', 'xh', 'xho'),
('Yao', '', 'yao'),
('Yapese', '', 'yap'),
('Yiddish', 'yi', 'yid'),
('Yoruba', 'yo', 'yor'),
('Yupik languages', '', 'ypk'),
('Zapotec', '', 'zap'),
('Blissymbols; Blissymbolics; Bliss', '', 'zbl'),
('Zenaga', '', 'zen'),
('Zhuang; Chuang', 'za', 'zha'),
('Zande languages', '', 'znd'),
('Zulu', 'zu', 'zul'),
('Zuni', '', 'zun'),
('No linguistic content; Not applicable', '', 'zxx'),
('Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki', '', 'zza'),
('Brazilian', 'po', 'pob')]
+131
View File
@@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import babelfish
from ..video import Episode, Movie
class Provider(object):
"""Base class for providers
If any configuration is possible for the provider, like credentials, it must take place during instantiation
:param \*\*kwargs: configuration
:raise: :class:`~subliminal.exceptions.ProviderConfigurationError` if there is a configuration error
"""
#: Supported BabelFish languages
languages = set()
#: Supported video types
video_types = (Episode, Movie)
#: Required hash, if any
required_hash = None
def __init__(self, **kwargs):
pass
def __enter__(self):
self.initialize()
return self
def __exit__(self, *args):
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 if you use the :keyword:`with` statement
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
"""
pass
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 if you use the :keyword:`with` statement
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
"""
pass
@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`'s `hashes` attribute.
:param video: the video to check
:type video: :class:`~subliminal.video.Video`
:return: `True` if the `video` and `languages` are 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, languages, *args, **kwargs):
"""Query the provider for subtitles
This method arguments match as much as possible the actual parameters for querying the provider
:param languages: languages to search for
:type languages: set of :class:`babelfish.Language`
:param \*args: other required arguments
:param \*\*kwargs: other optional arguments
:return: the subtitles
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
:raise: :class:`~subliminal.exceptions.ProviderError` if something unexpected occured
"""
raise NotImplementedError
def list_subtitles(self, video, languages):
"""List subtitles for the `video` with the given `languages`
This is a proxy for the :meth:`query` method. 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`
:return: the subtitles
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
:raise: :class:`~subliminal.exceptions.ProviderError` if something unexpected occured
"""
raise NotImplementedError
def download_subtitle(self, subtitle):
"""Download the `subtitle`
:param subtitle: subtitle to download
:type subtitle: :class:`~subliminal.subtitle.Subtitle`
:return: the subtitle text
:rtype: string
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
:raise: :class:`~subliminal.exceptions.InvalidSubtitle` if the downloaded subtitle is invalid
:raise: :class:`~subliminal.exceptions.ProviderError` if something unexpected occured
"""
raise NotImplementedError
def __repr__(self):
return '<%s [%r]>' % (self.__class__.__name__, self.video_types)
+191
View File
@@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import babelfish
import bs4
import charade
import requests
from . import Provider
from .. import __version__
from ..cache import region
from ..exceptions import ProviderConfigurationError, ProviderNotAvailable, InvalidSubtitle
from ..subtitle import Subtitle, is_valid_subtitle
from ..video import Episode
logger = logging.getLogger(__name__)
class Addic7edSubtitle(Subtitle):
provider_name = 'addic7ed'
def __init__(self, language, series, season, episode, title, version, hearing_impaired, download_link, referer):
super(Addic7edSubtitle, self).__init__(language, hearing_impaired)
self.series = series
self.season = season
self.episode = episode
self.title = title
self.version = version
self.download_link = download_link
self.referer = referer
def compute_matches(self, video):
matches = set()
# series
if video.series and 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 self.title.lower() == video.title.lower():
matches.add('title')
# 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')
return matches
class Addic7edProvider(Provider):
languages = {babelfish.Language('por', 'BR')} | {babelfish.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 = 'http://www.addic7ed.com'
def __init__(self, username=None, password=None):
if username is not None and password is None or username is None and password is not None:
raise ProviderConfigurationError('Username and password must be specified')
self.username = username
self.password = password
self.logged_in = False
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
# login
if self.username is not None and self.password is not None:
logger.debug('Logging in')
data = {'username': self.username, 'password': self.password, 'Submit': 'Log in'}
try:
r = self.session.post(self.server + '/dologin.php', data, timeout=10, allow_redirects=False)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code == 302:
logger.info('Logged in')
self.logged_in = True
else:
logger.error('Failed to login')
def terminate(self):
# logout
if self.logged_in:
try:
r = self.session.get(self.server + '/logout.php', timeout=10)
logger.info('Logged out')
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
self.session.close()
def get(self, url, params=None):
"""Make a GET request on `url` with the given parameters
:param string url: part of the URL to reach with the leading slash
:param params: params of the request
:return: the response
:rtype: :class:`bs4.BeautifulSoup`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
try:
r = self.session.get(self.server + url, params=params, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
return bs4.BeautifulSoup(r.content, ['permissive'])
@region.cache_on_arguments()
def get_show_ids(self):
"""Load the shows page with default series to show ids mapping
:return: series to show ids
:rtype: dict
"""
soup = self.get('/shows.php')
show_ids = {}
for html_show in soup.select('td.version > h3 > a[href^="/show/"]'):
show_ids[html_show.string.lower()] = int(html_show['href'][6:])
return show_ids
@region.cache_on_arguments()
def find_show_id(self, series):
"""Find a show id from the series
Use this only if the series is not in the dict returned by :meth:`get_show_ids`
:param string series: series of the episode
:return: the show id, if any
:rtype: int or None
"""
params = {'search': series, 'Submit': 'Search'}
logger.debug('Searching series %r', params)
suggested_shows = self.get('/search.php', params).select('span.titulo > a[href^="/show/"]')
if not suggested_shows:
logger.info('Series %r not found', series)
return None
return int(suggested_shows[0]['href'][6:])
def query(self, series, season):
show_ids = self.get_show_ids()
if series.lower() in show_ids:
show_id = show_ids[series.lower()]
else:
show_id = self.find_show_id(series.lower())
if show_id is None:
return []
params = {'show_id': show_id, 'season': season}
logger.debug('Searching subtitles %r', params)
link = '/show/{show_id}&season={season}'.format(**params)
soup = self.get(link)
subtitles = []
for row in soup('tr', class_='epeven completed'):
cells = row('td')
if cells[5].string != 'Completed':
logger.debug('Skipping incomplete subtitle')
continue
subtitles.append(Addic7edSubtitle(babelfish.Language.fromaddic7ed(cells[3].string), series, season,
int(cells[1].string), cells[2].string, cells[4].string,
bool(cells[6].string), cells[9].a['href'], link))
return subtitles
def list_subtitles(self, video, languages):
return [s for s in self.query(video.series, video.season)
if s.language in languages and s.episode == video.episode]
def download_subtitle(self, subtitle):
try:
r = self.session.get(self.server + subtitle.download_link, timeout=10,
headers={'Referer': self.server + subtitle.referer})
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
if r.headers['Content-Type'] == 'text/html':
raise ProviderNotAvailable('Download limit exceeded')
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+138
View File
@@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import urllib
import babelfish
import charade
import guessit
import requests
import xml.etree.ElementTree
from . import Provider
from .. import __version__
from ..cache import region
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
from ..subtitle import Subtitle, is_valid_subtitle, compute_guess_matches
from ..video import Episode
logger = logging.getLogger(__name__)
class BierDopjeSubtitle(Subtitle):
provider_name = 'bierdopje'
def __init__(self, language, season, episode, tvdb_id, series, filename, download_link):
super(BierDopjeSubtitle, self).__init__(language)
self.season = season
self.episode = episode
self.tvdb_id = tvdb_id
self.series = series
self.filename = filename
self.download_link = download_link
def compute_matches(self, video):
matches = set()
# tvdb_id
if video.tvdb_id and self.tvdb_id == video.tvdb_id:
matches.add('tvdb_id')
# series
if video.series and 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')
matches |= compute_guess_matches(video, guessit.guess_episode_info(self.filename + '.mkv'))
return matches
class BierDopjeProvider(Provider):
languages = {babelfish.Language(l) for l in ['eng', 'nld']}
video_types = (Episode,)
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
def terminate(self):
self.session.close()
def get(self, url, **params):
"""Make a GET request on the `url` formatted with `**params`
:param string url: API part of the URL to reach without the leading slash
:param \*\*params: format specs for the `url`
:return: the response
:rtype: :class:`xml.etree.ElementTree.Element`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
try:
r = self.session.get('http://api.bierdopje.com/A2B638AC5D804C2E/' + url.format(**params), timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code == 429:
raise ProviderNotAvailable('Too Many Requests')
elif r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
return xml.etree.ElementTree.fromstring(r.content)
@region.cache_on_arguments()
def find_show_id(self, series):
"""Find the show id from series name
:param string series: series of the episode
:return: show id
:rtype: int
"""
logger.debug('Searching for series %r', series)
root = self.get('FindShowByName/{series}', series=urllib.quote(series))
if root.find('response/status').text == 'false':
logger.info('Series %r not found', series)
return None
return int(root.find('response/results/result[1]/showid').text)
def query(self, language, season, episode, tvdb_id=None, series=None):
params = {'language': language.alpha2, 'season': season, 'episode': episode}
if tvdb_id is not None:
params['showid'] = tvdb_id
params['istvdbid'] = 'true'
elif series is not None:
show_id = self.find_show_id(series)
if show_id is None:
return []
params['showid'] = show_id
params['istvdbid'] = 'false'
else:
raise ValueError('Missing parameter tvdb_id or series')
logger.debug('Searching subtitles %r', params)
root = self.get('GetAllSubsFor/{showid}/{season}/{episode}/{language}/{istvdbid}', **params)
if root.find('response/status').text == 'false':
logger.debug('No subtitle found')
return []
logger.debug('Found subtitles %r', root.find('response/results'))
return [BierDopjeSubtitle(language, season, episode, tvdb_id, series, result.find('filename').text,
result.find('downloadlink').text) for result in root.find('response/results')]
def list_subtitles(self, video, languages):
subtitles = []
for language in languages:
subtitles.extend(self.query(language, video.season, video.episode, video.tvdb_id, video.series))
return subtitles
def download_subtitle(self, subtitle):
try:
r = self.session.get(subtitle.download_link, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code == 429:
raise ProviderNotAvailable('Too Many Requests')
elif r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+159
View File
@@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import base64
import logging
import os
import re
import xmlrpclib
import zlib
import babelfish
import charade
import guessit
from . import Provider
from .. import __version__
from ..exceptions import ProviderError, ProviderNotAvailable, InvalidSubtitle
from ..subtitle import Subtitle, is_valid_subtitle, compute_guess_matches
from ..video import Episode, Movie
logger = logging.getLogger(__name__)
class OpenSubtitlesSubtitle(Subtitle):
provider_name = 'opensubtitles'
series_re = re.compile('^"(?P<series_name>.*)" (?P<series_title>.*)$')
def __init__(self, language, hearing_impaired, id, matched_by, movie_kind, hash, movie_name, movie_release_name, movie_year,
movie_imdb_id, series_season, series_episode):
super(OpenSubtitlesSubtitle, self).__init__(language, hearing_impaired)
self.id = 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 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 compute_matches(self, video):
matches = set()
# episode
if isinstance(video, Episode) and self.movie_kind == 'episode':
# series
if video.series and self.series_name.lower() == video.series.lower():
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')
# guess
matches |= compute_guess_matches(video, guessit.guess_episode_info(self.movie_release_name + '.mkv'))
# movie
elif isinstance(video, Movie) and self.movie_kind == 'movie':
# year
if video.year and self.movie_year == video.year:
matches.add('year')
# guess
matches |= compute_guess_matches(video, guessit.guess_movie_info(self.movie_release_name + '.mkv'))
else:
logger.info('%r is not a valid movie_kind for %r', self.movie_kind, video)
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')
# title
if video.title and self.movie_name.lower() == video.title.lower():
matches.add('title')
return matches
class OpenSubtitlesProvider(Provider):
languages = {babelfish.Language.fromopensubtitles(l) for l in babelfish.CONVERTERS['opensubtitles'].codes}
def __init__(self):
self.server = xmlrpclib.ServerProxy('http://api.opensubtitles.org/xml-rpc')
self.token = None
def initialize(self):
try:
response = self.server.LogIn('', '', 'eng', 'subliminal v%s' % __version__)
except xmlrpclib.ProtocolError:
raise ProviderNotAvailable
if response['status'] != '200 OK':
raise ProviderError('Login failed with status %r' % response['status'])
self.token = response['token']
def terminate(self):
try:
response = self.server.LogOut(self.token)
except xmlrpclib.ProtocolError:
raise ProviderNotAvailable
if response['status'] != '200 OK':
raise ProviderError('Logout failed with status %r' % response['status'])
def query(self, languages, hash=None, size=None, imdb_id=None, query=None):
searches = []
if hash and size:
searches.append({'moviehash': hash, 'moviebytesize': str(size)})
if imdb_id:
searches.append({'imdbid': imdb_id})
if query:
searches.append({'query': query})
if not searches:
raise ValueError('One or more parameter missing')
for search in searches:
search['sublanguageid'] = ','.join(l.opensubtitles for l in languages)
logger.debug('Searching subtitles %r', searches)
try:
response = self.server.SearchSubtitles(self.token, searches)
except xmlrpclib.ProtocolError:
raise ProviderNotAvailable
if response['status'] != '200 OK':
raise ProviderError('Search failed with status %r' % response['status'])
if not response['data']:
logger.debug('No subtitle found')
return []
logger.debug('Found subtitles %r', response['data'])
return [OpenSubtitlesSubtitle(babelfish.Language.fromopensubtitles(r['SubLanguageID']),
bool(int(r['SubHearingImpaired'])), r['IDSubtitleFile'], r['MatchedBy'],
r['MovieKind'], r['MovieHash'], r['MovieName'], r['MovieReleaseName'],
int(r['MovieYear']) if r['MovieYear'] else None, int(r['IDMovieImdb']),
int(r['SeriesSeason']) if r['SeriesSeason'] else None,
int(r['SeriesEpisode']) if r['SeriesEpisode'] else None)
for r in response['data']]
def list_subtitles(self, video, languages):
query = None
if ('opensubtitles' not in video.hashes or not video.size) and not video.imdb_id:
query = video.name.split(os.sep)[-1]
return self.query(languages, hash=video.hashes.get('opensubtitles'), size=video.size, imdb_id=video.imdb_id, query=query)
def download_subtitle(self, subtitle):
try:
response = self.server.DownloadSubtitles(self.token, [subtitle.id])
except xmlrpclib.ProtocolError:
raise ProviderNotAvailable
if response['status'] != '200 OK':
raise ProviderError('Download failed with status %r' % response['status'])
if not response['data']:
raise ProviderError('Nothing to download')
subtitle_bytes = zlib.decompress(base64.b64decode(response['data'][0]['data']), 47)
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+81
View File
@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import babelfish
import charade
import requests
from . import Provider
from .. import __version__
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
from ..subtitle import Subtitle, is_valid_subtitle
logger = logging.getLogger(__name__)
class TheSubDBSubtitle(Subtitle):
provider_name = 'thesubdb'
def __init__(self, language, hash):
super(TheSubDBSubtitle, self).__init__(language)
self.hash = hash
def compute_matches(self, video):
matches = set()
# hash
if 'thesubdb' in video.hashes and video.hashes['thesubdb'] == self.hash:
matches.add('hash')
return matches
class TheSubDBProvider(Provider):
languages = {babelfish.Language.fromalpha2(l) for l in ['en', 'es', 'fr', 'it', 'nl', 'pl', 'pt', 'ro', 'sv', 'tr']}
required_hash = 'thesubdb'
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'SubDB/1.0 (subliminal/%s; https://github.com/Diaoul/subliminal)' %
__version__}
def terminate(self):
self.session.close()
def get(self, params):
"""Make a GET request on the server with the given parameters
:param params: params of the request
:return: the response
:rtype: :class:`requests.Response`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
try:
r = self.session.get('http://api.thesubdb.com', params=params, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
return r
def query(self, hash):
params = {'action': 'search', 'hash': hash}
logger.debug('Searching subtitles %r', params)
r = self.get(params)
if r.status_code == 404:
logger.debug('No subtitle found')
return []
elif r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
return [TheSubDBSubtitle(language, hash) for language in
{babelfish.Language.fromalpha2(l) for l in r.content.split(',')}]
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):
params = {'action': 'download', 'hash': subtitle.hash, 'language': subtitle.language.alpha2}
r = self.get(params)
if r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+166
View File
@@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import io
import logging
import re
import zipfile
import babelfish
import bs4
import charade
import requests
from . import Provider
from .. import __version__
from ..cache import region
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
from ..subtitle import Subtitle, is_valid_subtitle
from ..video import Episode
logger = logging.getLogger(__name__)
class TVsubtitlesSubtitle(Subtitle):
provider_name = 'tvsubtitles'
def __init__(self, language, series, season, episode, id, rip, release):
super(TVsubtitlesSubtitle, self).__init__(language)
self.series = series
self.season = season
self.episode = episode
self.id = id
self.rip = rip
self.release = release
def compute_matches(self, video):
matches = set()
# series
if video.series and 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')
# release_group
if video.release_group and self.release and video.release_group.lower() in self.release.lower():
matches.add('release_group')
# video_codec
if video.video_codec and self.release and (video.video_codec in self.release.lower()
or video.video_codec == 'h264' and 'x264' in self.release.lower()):
matches.add('video_codec')
# resolution
if video.resolution and self.rip and video.resolution in self.rip.lower():
matches.add('resolution')
return matches
class TVsubtitlesProvider(Provider):
languages = {babelfish.Language('por', 'BR')} | {babelfish.Language(l)
for l in ['ara', 'bul', 'ces', 'dan', 'deu', 'ell', 'eng', 'fin', 'fra', 'hun', 'ita', 'jpn', 'kor',
'nld', 'pol', 'por', 'ron', 'rus', 'spa', 'swe', 'tur', 'ukr', 'zho']}
video_types = (Episode,)
server = 'http://www.tvsubtitles.net'
episode_id_re = re.compile('^episode-(\d+)\.html$')
subtitle_re = re.compile('^\/subtitle-(\d+)\.html$')
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
def terminate(self):
self.session.close()
def request(self, url, params=None, data=None, method='GET'):
"""Make a `method` request on `url` with the given parameters
:param string url: part of the URL to reach with the leading slash
:param dict params: params of the request
:param dict data: data of the request
:return: the response
:rtype: :class:`bs4.BeautifulSoup`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
try:
r = self.session.request(method, self.server + url, params=params, data=data, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
return bs4.BeautifulSoup(r.content, ['permissive'])
@region.cache_on_arguments()
def find_show_id(self, series):
"""Find a show id from the series
:param string series: series of the episode
:return: the show id, if any
:rtype: int or None
"""
data = {'q': series}
logger.debug('Searching series %r', data)
soup = self.request('/search.php', data=data, method='POST')
links = soup.select('div.left li div a[href^="/tvshow-"]')
if not links:
logger.info('Series %r not found', series)
return None
return int(links[0]['href'][8:-5])
@region.cache_on_arguments()
def find_episode_ids(self, show_id, season):
"""Find 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
"""
params = {'show_id': show_id, 'season': season}
logger.debug('Searching episodes %r', params)
soup = self.request('/tvshow-{show_id}-{season}.html'.format(**params))
episode_ids = {}
for row in soup.select('table#table5 tr'):
if not row('a', href=self.episode_id_re):
continue
cells = row('td')
episode_ids[int(cells[0].string.split('x')[1])] = int(cells[1].a['href'][8:-5])
return episode_ids
def query(self, series, season, episode):
show_id = self.find_show_id(series.lower())
if show_id is None:
return []
episode_ids = self.find_episode_ids(show_id, season)
if episode not in episode_ids:
logger.info('Episode %d not found', episode)
return []
params = {'episode_id': episode_ids[episode]}
logger.debug('Searching episode %r', params)
soup = self.request('/episode-{episode_id}.html'.format(**params))
return [TVsubtitlesSubtitle(babelfish.Language.fromtvsubtitles(row.h5.img['src'][13:-4]), series, season,
episode, row['href'][10:-5], row.find('p', title='rip').text.strip() or None,
row.find('p', title='release').text.strip() or None)
for row in soup('a', href=self.subtitle_re)]
def list_subtitles(self, video, languages):
return [s for s in self.query(video.series, video.season, video.episode) if s.language in languages]
def download_subtitle(self, subtitle):
try:
r = self.session.get(self.server + '/download-{subtitle_id}.html'.format(subtitle_id=subtitle.id),
timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
with zipfile.ZipFile(io.BytesIO(r.content)) as zf:
if len(zf.namelist()) > 1:
raise ProviderError('More than one file to unzip')
subtitle_bytes = zf.read(zf.namelist()[0])
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals
from sympy import Eq, symbols, solve
# Symbols
release_group, resolution, video_codec, audio_codec = symbols('release_group resolution video_codec audio_codec')
imdb_id, hash, title, series, tvdb_id, season, episode = symbols('imdb_id hash title series tvdb_id season episode')
year = symbols('year')
def get_episode_equations():
"""Get the score equations for a :class:`~subliminal.video.Episode`
The equations are the following:
1. hash = resolution + video_codec + audio_codec + series + season + episode + release_group
2. series = resolution + video_codec + audio_codec + season + episode + 1
3. tvdb_id = series
4. season = resolution + video_codec + audio_codec + 1
5. imdb_id = series + season + episode
6. resolution = video_codec
7. video_codec = 2 * audio_codec
8. title = season + episode
9. season = episode
10. release_group = season
11. audio_codec = 1
:return: the score equations for an episode
:rtype: list of :class:`sympy.Eq`
"""
equations = []
equations.append(Eq(hash, resolution + video_codec + audio_codec + series + season + episode + release_group))
equations.append(Eq(series, resolution + video_codec + audio_codec + season + episode + release_group))
equations.append(Eq(tvdb_id, series))
equations.append(Eq(season, resolution + video_codec + audio_codec + 1))
equations.append(Eq(imdb_id, series + season + episode))
equations.append(Eq(resolution, video_codec))
equations.append(Eq(video_codec, 2 * audio_codec))
equations.append(Eq(title, season + episode))
equations.append(Eq(season, episode))
equations.append(Eq(release_group, season))
equations.append(Eq(audio_codec, 1))
return equations
def get_movie_equations():
"""Get the score equations for a :class:`~subliminal.video.Movie`
The equations are the following:
1. hash = resolution + video_codec + audio_codec + title + year + release_group
2. imdb_id = hash
3. resolution = video_codec
4. video_codec = 2 * audio_codec
5. title = resolution + video_codec + audio_codec + year + 1
6. release_group = resolution + video_codec + audio_codec + 1
7. year = release_group + 1
8. audio_codec = 1
:return: the score equations for a movie
:rtype: list of :class:`sympy.Eq`
"""
equations = []
equations.append(Eq(hash, resolution + video_codec + audio_codec + title + year + release_group))
equations.append(Eq(imdb_id, hash))
equations.append(Eq(resolution, video_codec))
equations.append(Eq(video_codec, 2 * audio_codec))
equations.append(Eq(title, resolution + video_codec + audio_codec + year + 1))
equations.append(Eq(video_codec, 2 * audio_codec))
equations.append(Eq(release_group, resolution + video_codec + audio_codec + 1))
equations.append(Eq(year, release_group + 1))
equations.append(Eq(audio_codec, 1))
return equations
if __name__ == '__main__':
print(solve(get_episode_equations(), [release_group, resolution, video_codec, audio_codec, imdb_id,
hash, series, tvdb_id, season, episode, title]))
print(solve(get_movie_equations(), [release_group, resolution, video_codec, audio_codec, imdb_id,
hash, title, year]))
-214
View File
@@ -1,214 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from ..exceptions import MissingLanguageError, DownloadFailedError
import logging
import os
import requests
import threading
__all__ = ['ServiceBase', 'ServiceConfig']
logger = logging.getLogger(__name__)
class ServiceBase(object):
"""Service base class
:param config: service configuration
:type config: :class:`ServiceConfig`
"""
#: URL to the service server
server_url = ''
#: User Agent for any HTTP-based requests
user_agent = 'subliminal v0.5'
#: Whether based on an API or not
api_based = False
#: Timeout for web requests
timeout = 5
#: Lock for cache interactions
lock = threading.Lock()
#: Mapping to Service's language codes and subliminal's
languages = {}
#: Whether the mapping is reverted or not
reverted_languages = False
#: Accepted video classes (:class:`~subliminal.videos.Episode`, :class:`~subliminal.videos.Movie`, :class:`~subliminal.videos.UnknownVideo`)
videos = []
#: Whether the video has to exist or not
require_video = False
def __init__(self, config=None):
self.config = config or ServiceConfig()
def __enter__(self):
self.init()
return self
def __exit__(self, *args):
self.terminate()
def init(self):
"""Initialize connection"""
logger.debug(u'Initializing %s' % self.__class__.__name__)
self.session = requests.session(timeout=10, headers={'User-Agent': self.user_agent})
def terminate(self):
"""Terminate connection"""
logger.debug(u'Terminating %s' % self.__class__.__name__)
def query(self, *args):
"""Make the actual query"""
pass
def list(self, video, languages):
"""List subtitles"""
pass
def download(self, subtitle):
"""Download a subtitle"""
self.download_file(subtitle.link, subtitle.path)
@classmethod
def available_languages(cls):
"""Available languages in the Service
:return: available languages
:rtype: set
"""
if not cls.reverted_languages:
return set(cls.languages.keys())
return set(cls.languages.values())
@classmethod
def check_validity(cls, video, languages):
"""Check for video and languages validity in the Service
:param video: the video to check
:type video: :class:`~subliminal.videos.video`
:param set languages: languages to check
:rtype: bool
"""
languages &= cls.available_languages()
if not languages:
logger.debug(u'No language available for service %s' % cls.__class__.__name__.lower())
return False
if not cls.is_valid_video(video):
logger.debug(u'%r is not valid for service %s' % (video, cls.__class__.__name__.lower()))
return False
return True
@classmethod
def is_valid_video(cls, video):
"""Check if video is valid in the Service
:param video: the video to check
:type video: :class:`~subliminal.videos.Video`
:rtype: bool
"""
if cls.require_video and not video.exists:
return False
if not isinstance(video, tuple(cls.videos)):
return False
return True
@classmethod
def is_valid_language(cls, language):
"""Check if language is valid in the Service
:param string language: the language to check
:rtype: bool
"""
if language in cls.available_languages():
return True
return False
@classmethod
def get_revert_language(cls, language):
"""ISO-639-1 language code from service language code
:param string language: service language code
:return: ISO-639-1 language code
:rtype: string
"""
if not cls.reverted_languages and language in cls.languages.values():
return [k for k, v in cls.languages.iteritems() if v == language][0]
if cls.reverted_languages and language in cls.languages.keys():
return cls.languages[language]
raise MissingLanguageError(language)
@classmethod
def get_language(cls, language):
"""Service language code from ISO-639-1 language code
:param string language: ISO-639-1 language code
:return: service language code
:rtype: string
"""
if not cls.reverted_languages and language in cls.languages.keys():
return cls.languages[language]
if cls.reverted_languages and language in cls.languages.values():
return [k for k, v in cls.languages.iteritems() if v == language][0]
raise MissingLanguageError(language)
def download_file(self, url, filepath):
"""Attempt to download a file and remove it in case of failure
:param string url: URL to download
:param string filepath: destination path
"""
logger.info(u'Downloading %s' % url)
try:
r = self.session.get(url, headers={'Referer': url, 'User-Agent': self.user_agent})
with open(filepath, 'wb') as f:
f.write(r.content)
except Exception as e:
logger.error(u'Download %s failed: %s' % (url, e))
if os.path.exists(filepath):
os.remove(filepath)
raise DownloadFailedError(str(e))
logger.debug(u'Download finished for file %s. Size: %s' % (filepath, os.path.getsize(filepath)))
class ServiceConfig(object):
"""Configuration for any :class:`Service`
:param bool multi: whether to download one subtitle per language or not
:param string cache_dir: cache directory
"""
def __init__(self, multi=False, cache_dir=None):
self.multi = multi
self.cache_dir = cache_dir
def __repr__(self):
return 'ServiceConfig(%r, %s)' % (self.multi, self.cache_dir)
-122
View File
@@ -1,122 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..exceptions import ServiceError
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode
from ..utils import to_unicode
import BeautifulSoup
import logging
import os.path
import urllib
try:
import cPickle as pickle
except ImportError:
import pickle
logger = logging.getLogger(__name__)
class BierDopje(ServiceBase):
server_url = 'http://api.bierdopje.com/A2B638AC5D804C2E/'
api_based = True
languages = {'en': 'en', 'nl': 'nl'}
reverted_languages = False
videos = [Episode]
require_video = False
def __init__(self, config=None):
super(BierDopje, self).__init__(config)
self.showids = {}
if self.config and self.config.cache_dir:
self.init_cache()
def init_cache(self):
logger.debug(u'Initializing cache...')
if not self.config or not self.config.cache_dir:
raise ServiceError('Cache directory is required')
self.showids_cache = os.path.join(self.config.cache_dir, 'bierdopje_showids.cache')
if not os.path.exists(self.showids_cache):
self.save_cache()
def save_cache(self):
logger.debug(u'Saving showids to cache...')
with self.lock:
with open(self.showids_cache, 'w') as f:
pickle.dump(self.showids, f)
def load_cache(self):
logger.debug(u'Loading showids from cache...')
with self.lock:
with open(self.showids_cache, 'r') as f:
self.showids = pickle.load(f)
def query(self, season, episode, languages, filepath, tvdbid=None, series=None):
self.load_cache()
if series:
if series.lower() in self.showids: # from cache
request_id = self.showids[series.lower()]
logger.debug(u'Retreived showid %d for %s from cache' % (request_id, series))
else: # query to get showid
logger.debug(u'Getting showid from show name %s...' % series)
r = self.session.get('%sGetShowByName/%s' % (self.server_url, urllib.quote(series.lower())))
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
soup = BeautifulSoup.BeautifulStoneSoup(r.content)
if soup.status.contents[0] == 'false':
logger.debug(u'Could not find show %s' % series)
return []
request_id = int(soup.showid.contents[0])
self.showids[series.lower()] = request_id
self.save_cache()
request_source = 'showid'
request_is_tvdbid = 'false'
elif tvdbid:
request_id = tvdbid
request_source = 'tvdbid'
request_is_tvdbid = 'true'
else:
raise ServiceError('One or more parameter missing')
subtitles = []
for language in languages:
logger.debug(u'Getting subtitles for %s %d season %d episode %d with language %s' % (request_source, request_id, season, episode, language))
r = self.session.get('%sGetAllSubsFor/%s/%s/%s/%s/%s' % (self.server_url, request_id, season, episode, language, request_is_tvdbid))
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
soup = BeautifulSoup.BeautifulStoneSoup(r.content)
if soup.status.contents[0] == 'false':
logger.debug(u'Could not find subtitles for %s %d season %d episode %d with language %s' % (request_source, request_id, season, episode, language))
continue
path = get_subtitle_path(filepath, language, self.config.multi)
for result in soup.results('result'):
subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link=result.downloadlink.contents[0],
release=to_unicode(result.filename.contents[0]))
subtitles.append(subtitle)
return subtitles
def list(self, video, languages):
if not self.check_validity(video, languages):
return []
results = self.query(video.season, video.episode, languages, video.path or video.release, video.tvdbid, video.series)
return results
Service = BierDopje
-143
View File
@@ -1,143 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..exceptions import ServiceError, DownloadFailedError
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode, Movie
from ..utils import to_unicode
import gzip
import logging
import os.path
import xmlrpclib
logger = logging.getLogger(__name__)
class OpenSubtitles(ServiceBase):
server_url = 'http://api.opensubtitles.org/xml-rpc'
api_based = True
languages = {'aa': 'aar', 'ab': 'abk', 'af': 'afr', 'ak': 'aka', 'sq': 'alb', 'am': 'amh', 'ar': 'ara',
'an': 'arg', 'hy': 'arm', 'as': 'asm', 'av': 'ava', 'ae': 'ave', 'ay': 'aym', 'az': 'aze',
'ba': 'bak', 'bm': 'bam', 'eu': 'baq', 'be': 'bel', 'bn': 'ben', 'bh': 'bih', 'bi': 'bis',
'bs': 'bos', 'br': 'bre', 'bg': 'bul', 'my': 'bur', 'ca': 'cat', 'ch': 'cha', 'ce': 'che',
'zh': 'chi', 'cu': 'chu', 'cv': 'chv', 'kw': 'cor', 'co': 'cos', 'cr': 'cre', 'cs': 'cze',
'da': 'dan', 'dv': 'div', 'nl': 'dut', 'dz': 'dzo', 'en': 'eng', 'eo': 'epo', 'et': 'est',
'ee': 'ewe', 'fo': 'fao', 'fj': 'fij', 'fi': 'fin', 'fr': 'fre', 'fy': 'fry', 'ff': 'ful',
'ka': 'geo', 'de': 'ger', 'gd': 'gla', 'ga': 'gle', 'gl': 'glg', 'gv': 'glv', 'el': 'ell',
'gn': 'grn', 'gu': 'guj', 'ht': 'hat', 'ha': 'hau', 'he': 'heb', 'hz': 'her', 'hi': 'hin',
'ho': 'hmo', 'hr': 'hrv', 'hu': 'hun', 'ig': 'ibo', 'is': 'ice', 'io': 'ido', 'ii': 'iii',
'iu': 'iku', 'ie': 'ile', 'ia': 'ina', 'id': 'ind', 'ik': 'ipk', 'it': 'ita', 'jv': 'jav',
'ja': 'jpn', 'kl': 'kal', 'kn': 'kan', 'ks': 'kas', 'kr': 'kau', 'kk': 'kaz', 'km': 'khm',
'ki': 'kik', 'rw': 'kin', 'ky': 'kir', 'kv': 'kom', 'kg': 'kon', 'ko': 'kor', 'kj': 'kua',
'ku': 'kur', 'lo': 'lao', 'la': 'lat', 'lv': 'lav', 'li': 'lim', 'ln': 'lin', 'lt': 'lit',
'lb': 'ltz', 'lu': 'lub', 'lg': 'lug', 'mk': 'mac', 'mh': 'mah', 'ml': 'mal', 'mi': 'mao',
'mr': 'mar', 'ms': 'may', 'mg': 'mlg', 'mt': 'mlt', 'mo': 'mol', 'mn': 'mon', 'na': 'nau',
'nv': 'nav', 'nr': 'nbl', 'nd': 'nde', 'ng': 'ndo', 'ne': 'nep', 'nn': 'nno', 'nb': 'nob',
'no': 'nor', 'ny': 'nya', 'oc': 'oci', 'oj': 'oji', 'or': 'ori', 'om': 'orm', 'os': 'oss',
'pa': 'pan', 'fa': 'per', 'pi': 'pli', 'pl': 'pol', 'pt': 'por', 'ps': 'pus', 'qu': 'que',
'rm': 'roh', 'rn': 'run', 'ru': 'rus', 'sg': 'sag', 'sa': 'san', 'sr': 'scc', 'si': 'sin',
'sk': 'slo', 'sl': 'slv', 'se': 'sme', 'sm': 'smo', 'sn': 'sna', 'sd': 'snd', 'so': 'som',
'st': 'sot', 'es': 'spa', 'sc': 'srd', 'ss': 'ssw', 'su': 'sun', 'sw': 'swa', 'sv': 'swe',
'ty': 'tah', 'ta': 'tam', 'tt': 'tat', 'te': 'tel', 'tg': 'tgk', 'tl': 'tgl', 'th': 'tha',
'bo': 'tib', 'ti': 'tir', 'to': 'ton', 'tn': 'tsn', 'ts': 'tso', 'tk': 'tuk', 'tr': 'tur',
'tw': 'twi', 'ug': 'uig', 'uk': 'ukr', 'ur': 'urd', 'uz': 'uzb', 've': 'ven', 'vi': 'vie',
'vo': 'vol', 'cy': 'wel', 'wa': 'wln', 'wo': 'wol', 'xh': 'xho', 'yi': 'yid', 'yo': 'yor',
'za': 'zha', 'zu': 'zul', 'ro': 'rum', 'po': 'pob', 'un': 'unk', 'ay': 'ass'}
reverted_languages = False
videos = [Episode, Movie]
require_video = False
confidence_order = ['moviehash', 'imdbid', 'fulltext']
def __init__(self, config=None):
super(OpenSubtitles, self).__init__(config)
self.server = xmlrpclib.ServerProxy(self.server_url)
self.token = None
def init(self):
super(OpenSubtitles, self).init()
result = self.server.LogIn('', '', 'eng', self.user_agent)
if result['status'] != '200 OK':
raise ServiceError('Login failed')
self.token = result['token']
def terminate(self):
super(OpenSubtitles, self).terminate()
if self.token:
self.server.LogOut(self.token)
def query(self, filepath, languages, moviehash=None, size=None, imdbid=None, query=None):
searches = []
if moviehash and size:
searches.append({'moviehash': moviehash, 'moviebytesize': size})
if imdbid:
searches.append({'imdbid': imdbid})
if query:
searches.append({'query': query})
if not searches:
raise ServiceError('One or more parameter missing')
for search in searches:
search['sublanguageid'] = ','.join([self.get_language(l) for l in languages])
logger.debug(u'Getting subtitles %r with token %s' % (searches, self.token))
results = self.server.SearchSubtitles(self.token, searches)
if not results['data']:
logger.debug(u'Could not find subtitles for %r with token %s' % (searches, self.token))
return []
subtitles = []
for result in results['data']:
language = self.get_revert_language(result['SubLanguageID'])
path = get_subtitle_path(filepath, language, self.config.multi)
confidence = 1 - float(self.confidence_order.index(result['MatchedBy'])) / float(len(self.confidence_order))
subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link=result['SubDownloadLink'],
release=to_unicode(result['SubFileName']), confidence=confidence)
subtitles.append(subtitle)
return subtitles
def list(self, video, languages):
if not self.check_validity(video, languages):
return []
results = []
if video.exists:
results = self.query(video.path or video.release, languages, moviehash=video.hashes['OpenSubtitles'], size=str(video.size))
elif video.imdbid:
results = self.query(video.path or video.release, languages, imdbid=video.imdbid)
elif isinstance(video, Episode):
results = self.query(video.path or video.release, languages, query=video.series)
elif isinstance(video, Movie):
results = self.query(video.path or video.release, languages, query=video.title)
return results
def download(self, subtitle):
#TODO: Use OpenSubtitles DownloadSubtitles method
try:
self.download_file(subtitle.link, subtitle.path + '.gz')
with open(subtitle.path, 'wb') as dump:
gz = gzip.open(subtitle.path + '.gz')
dump.write(gz.read())
gz.close()
except Exception as e:
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
raise DownloadFailedError(str(e))
finally:
if os.path.exists(subtitle.path + '.gz'):
os.remove(subtitle.path + '.gz')
return subtitle
Service = OpenSubtitles
-99
View File
@@ -1,99 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..exceptions import ServiceError
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode, Movie
from subliminal.utils import get_keywords, split_keyword
import BeautifulSoup
import logging
import re
import urllib
logger = logging.getLogger(__name__)
class SubsWiki(ServiceBase):
server_url = 'http://www.subswiki.com'
api_based = False
languages = {u'English (US)': 'en', u'English (UK)': 'en', u'English': 'en', u'French': 'fr', u'Brazilian': 'po',
u'Portuguese': 'pt', u'Español (Latinoamérica)': 'es', u'Español (España)': 'es', u'Español': 'es',
u'Italian': 'it', u'Català': 'ca'}
reverted_languages = True
videos = [Episode, Movie]
require_video = False
release_pattern = re.compile('\nVersion (.+), ([0-9]+).([0-9])+ MBs')
def list(self, video, languages):
if not self.check_validity(video, languages):
return []
results = []
if isinstance(video, Episode):
results = self.query(video.path or video.release, languages, get_keywords(video.guess), series=video.series, season=video.season, episode=video.episode)
elif isinstance(video, Movie) and video.year:
results = self.query(video.path or video.release, languages, get_keywords(video.guess), movie=video.title, year=video.year)
return results
def query(self, filepath, languages, keywords=None, series=None, season=None, episode=None, movie=None, year=None):
if series and season and episode:
request_series = series.lower().replace(' ', '_')
if isinstance(request_series, unicode):
request_series = request_series.encode('utf-8')
logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
r = self.session.get('%s/serie/%s/%s/%s/' % (self.server_url, urllib.quote(request_series), season, episode))
if r.status_code == 404:
logger.debug(u'Could not find subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
return []
elif movie and year:
request_movie = movie.title().replace(' ', '_')
if isinstance(request_movie, unicode):
request_movie = request_movie.encode('utf-8')
logger.debug(u'Getting subtitles for %s (%d) with languages %r' % (movie, year, languages))
r = self.session.get('%s/film/%s_(%d)' % (self.server_url, urllib.quote(request_movie), year))
if r.status_code == 404:
logger.debug(u'Could not find subtitles for %s (%d) with languages %r' % (movie, year, languages))
return []
else:
raise ServiceError('One or more parameter missing')
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
soup = BeautifulSoup.BeautifulSoup(r.content)
subtitles = []
for sub in soup('td', {'class': 'NewsTitle'}):
sub_keywords = split_keyword(self.release_pattern.search(sub.contents[1]).group(1).lower())
if not keywords & sub_keywords:
logger.debug(u'None of subtitle keywords %r in %r' % (sub_keywords, keywords))
continue
for html_language in sub.parent.parent.findAll('td', {'class': 'language'}):
language = self.get_revert_language(html_language.string.strip())
if not language in languages:
logger.debug(u'Language %r not in wanted languages %r' % (language, languages))
continue
html_status = html_language.findNextSibling('td')
status = html_status.find('strong').string.strip()
if status != 'Completed':
logger.debug(u'Wrong subtitle status %s' % status)
continue
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link='%s%s' % (self.server_url, html_status.findNext('td').find('a')['href']))
subtitles.append(subtitle)
return subtitles
Service = SubsWiki
-83
View File
@@ -1,83 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode
from subliminal.utils import get_keywords, split_keyword
import BeautifulSoup
import logging
import re
import unicodedata
import urllib
logger = logging.getLogger(__name__)
class Subtitulos(ServiceBase):
server_url = 'http://www.subtitulos.es'
api_based = False
languages = {u'English (US)': 'en', u'English (UK)': 'en', u'English': 'en', u'French': 'fr', u'Brazilian': 'po',
u'Portuguese': 'pt', u'Español (Latinoamérica)': 'es', u'Español (España)': 'es', u'Español': 'es',
u'Italian': 'it', u'Català': 'ca'}
reverted_languages = True
videos = [Episode]
require_video = False
release_pattern = re.compile('Versi&oacute;n (.+) ([0-9]+).([0-9])+ megabytes')
def list(self, video, languages):
if not self.check_validity(video, languages):
return []
results = self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode)
return results
def query(self, filepath, languages, keywords, series, season, episode):
request_series = series.lower().replace(' ', '_')
if isinstance(request_series, unicode):
request_series = unicodedata.normalize('NFKD', request_series).encode('ascii', 'ignore')
logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
r = self.session.get('%s/%s/%sx%.2d' % (self.server_url, urllib.quote(request_series), season, episode))
if r.status_code == 404:
logger.debug(u'Could not find subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
return []
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
soup = BeautifulSoup.BeautifulSoup(r.content)
subtitles = []
for sub in soup('div', {'id': 'version'}):
sub_keywords = split_keyword(self.release_pattern.search(sub.find('p', {'class': 'title-sub'}).contents[1]).group(1).lower())
if not keywords & sub_keywords:
logger.debug(u'None of subtitle keywords %r in %r' % (sub_keywords, keywords))
continue
for html_language in sub.findAllNext('ul', {'class': 'sslist'}):
language = self.get_revert_language(html_language.findNext('li', {'class': 'li-idioma'}).find('strong').contents[0].string.strip())
if not language in languages:
logger.debug(u'Language %r not in wanted languages %r' % (language, languages))
continue
html_status = html_language.findNext('li', {'class': 'li-estado green'})
status = html_status.contents[0].string.strip()
if status != 'Completado':
logger.debug(u'Wrong subtitle status %s' % status)
continue
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link=html_status.findNext('span', {'class': 'descargar green'}).find('a')['href'], keywords=sub_keywords)
subtitles.append(subtitle)
return subtitles
Service = Subtitulos
-65
View File
@@ -1,65 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode, Movie, UnknownVideo
import logging
logger = logging.getLogger(__name__)
class TheSubDB(ServiceBase):
server_url = 'http://api.thesubdb.com/' # for testing purpose, use http://sandbox.thesubdb.com/ instead
user_agent = 'SubDB/1.0 (subliminal/0.5; https://github.com/Diaoul/subliminal)' # defined by the API
api_based = True
languages = {'af': 'af', 'cs': 'cs', 'da': 'da', 'de': 'de', 'en': 'en', 'es': 'es', 'fi': 'fi',
'fr': 'fr', 'hu': 'hu', 'id': 'id', 'it': 'it', 'la': 'la', 'nl': 'nl', 'no': 'no',
'oc': 'oc', 'pl': 'pl', 'pt': 'pt', 'ro': 'ro', 'ru': 'ru', 'sl': 'sl', 'sr': 'sr',
'sv': 'sv', 'tr': 'tr'} # list available with the API at http://sandbox.thesubdb.com/?action=languages
reverted_languages = False
videos = [Movie, Episode, UnknownVideo]
require_video = True
def list(self, video, languages):
if not self.check_validity(video, languages):
return []
results = self.query(video.path, video.hashes['TheSubDB'], languages)
return results
def query(self, filepath, moviehash, languages):
r = self.session.get(self.server_url, params={'action': 'search', 'hash': moviehash})
if r.status_code == 404:
logger.debug(u'Could not find subtitles for hash %s' % moviehash)
return []
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
available_languages = set([self.get_revert_language(l) for l in r.content.split(',')])
languages &= available_languages
if not languages:
logger.debug(u'Could not find subtitles for hash %s with languages %r (only %r available)' % (moviehash, languages, available_languages))
return []
subtitles = []
for language in languages:
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link='%s?action=download&hash=%s&language=%s' % (self.server_url, moviehash, self.get_language(language)))
subtitles.append(subtitle)
return subtitles
Service = TheSubDB
+153
View File
@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import os.path
import babelfish
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`
:param bool hearing_impaired: `True` if the subtitle is hearing impaired, `False` otherwise
"""
def __init__(self, language, hearing_impaired=False):
self.language = language
self.hearing_impaired = hearing_impaired
def compute_matches(self, video):
"""Compute the matches of the subtitle against the `video`
:param video: the video to compute the matches against
:type video: :class:`~subliminal.video.Video`
:return: matches of the subtitle
:rtype: set
"""
raise NotImplementedError
def compute_score(self, video):
"""Compute the score of the subtitle against the `video`
There are equivalent matches so that a provider can match one element or its equivalent. This is
to give all provider a chance to have a score in the same range without hurting quality.
* Matching :class:`~subliminal.video.Video`'s `hashes` is equivalent to matching everything else
* Matching :class:`~subliminal.video.Episode`'s `season` and `episode`
is equivalent to matching :class:`~subliminal.video.Episode`'s `title`
* Matching :class:`~subliminal.video.Episode`'s `tvdb_id` is equivalent to matching
:class:`~subliminal.video.Episode`'s `series`
:param video: the video to compute the score against
:type video: :class:`~subliminal.video.Video`
:return: score of the subtitle
:rtype: int
"""
score = 0
# compute matches
initial_matches = self.compute_matches(video)
matches = initial_matches.copy()
# hash is the perfect match
if 'hash' in matches:
score = video.scores['hash']
else:
# remove equivalences
if isinstance(video, Episode):
if 'imdb_id' in matches:
matches -= {'series', 'tvdb_id', 'season', 'episode', 'title'}
if 'tvdb_id' in matches:
matches -= {'series'}
if 'title' in matches:
matches -= {'season', 'episode'}
# add other scores
score += sum((video.scores[match] for match in matches))
logger.info('Computed score %d with matches %r', score, initial_matches)
return score
def __repr__(self):
return '<%s [%r]>' % (self.__class__.__name__, self.language)
def get_subtitle_path(video_path, language=None):
"""Create the subtitle path from the given `video_path` and `language`
:param string video_path: path to the video
:param language: language of the subtitle to put in the path
:type language: :class:`babelfish.Language` or None
:return: path of the subtitle
:rtype: string
"""
subtitle_path = os.path.splitext(video_path)[0]
if language is not None:
try:
return subtitle_path + '.%s.%s' % (language.alpha2, 'srt')
except babelfish.ConvertError:
return subtitle_path + '.%s.%s' % (language.alpha3, 'srt')
return subtitle_path + '.srt'
def is_valid_subtitle(subtitle_text):
"""Check if a subtitle text is a valid SubRip format
:return: `True` if the subtitle is valid, `False` otherwise
:rtype: bool
"""
try:
pysrt.from_string(subtitle_text, error_handling=pysrt.ERROR_RAISE)
return True
except pysrt.Error:
return False
def compute_guess_matches(video, guess):
"""Compute matches between a `video` and a `guess`
:param video: the video to compute the matches on
:type video: :class:`~subliminal.video.Video`
:param guess: the guess to compute the matches on
:type guess: :class:`guessit.Guess`
:return: matches of the `guess`
:rtype: set
"""
matches = set()
if isinstance(video, Episode):
# Series
if video.series and 'series' in guess and guess['series'].lower() == video.series.lower():
matches.add('series')
# Season
if video.season and 'seasonNumber' in guess and guess['seasonNumber'] == video.season:
matches.add('season')
# Episode
if video.episode and 'episodeNumber' in guess and guess['episodeNumber'] == video.episode:
matches.add('episode')
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 guess['title'].lower() == video.title.lower():
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')
# Screen size
if video.resolution and 'screenSize' in guess and guess['screenSize'] == video.resolution:
matches.add('resolution')
# 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
-130
View File
@@ -1,130 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from .languages import list_languages, convert_language
import os.path
__all__ = ['Subtitle', 'EmbeddedSubtitle', 'ExternalSubtitle', 'ResultSubtitle', 'get_subtitle_path']
#: Subtitles extensions
EXTENSIONS = ['.srt', '.sub', '.txt']
class Subtitle(object):
"""Base class for subtitles
:param string path: path to the subtitle
:param string language: language of the subtitle (second element of :class:`~subliminal.languages.LANGUAGES`)
"""
def __init__(self, path, language):
self.path = path
self.language = language
@property
def exists(self):
"""Whether the subtitle exists or not"""
if self.path:
return os.path.exists(self.path)
return False
class EmbeddedSubtitle(Subtitle):
"""Subtitle embedded in a container
:param string path: path to the subtitle
:param string language: language of the subtitle (second element of :class:`~subliminal.languages.LANGUAGES`)
:param int track_id: id of the subtitle track in the container
"""
def __init__(self, path, language, track_id):
super(EmbeddedSubtitle, self).__init__(path, language)
self.track_id = track_id
@classmethod
def from_enzyme(cls, path, subtitle):
language = convert_language(subtitle.language, 1, 2)
return cls(path, language, subtitle.trackno)
class ExternalSubtitle(Subtitle):
"""Subtitle in a file next to the video file"""
@classmethod
def from_path(cls, path):
"""Create an :class:`ExternalSubtitle` from path"""
extension = ''
for e in EXTENSIONS:
if path.endswith(e):
extension = e
break
if not extension:
raise ValueError('Not a supported subtitle extension')
language = os.path.splitext(path[:len(path) - len(extension)])[1][1:]
if not language in list_languages(1):
language = None
return cls(path, language)
class ResultSubtitle(ExternalSubtitle):
"""Subtitle found using :mod:`~subliminal.services`
:param string path: path to the subtitle
:param string language: language of the subtitle (second element of :class:`~subliminal.languages.LANGUAGES`)
:param string service: name of the service
:param string link: download link for the subtitle
:param string release: release name of the video
:param float confidence: confidence that the subtitle matches the video according to the service
:param set keywords: keywords that describe the subtitle
"""
def __init__(self, path, language, service, link, release=None, confidence=1, keywords=set()):
super(ResultSubtitle, self).__init__(path, language)
self.service = service
self.link = link
self.release = release
self.confidence = confidence
self.keywords = keywords
@property
def single(self):
"""Whether this is a single subtitle or not. A single subtitle does not have
a language indicator in its file name
:rtype: bool
"""
extension = os.path.splitext(self.path)[0]
language = os.path.splitext(self.path[:len(self.path) - len(extension)])[1][1:]
if not language in list_languages(1):
return True
return False
def __repr__(self):
return 'ResultSubtitle(%s, %s, %.2f, %s)' % (self.language, self.service, self.confidence, self.release)
def get_subtitle_path(video_path, language, multi):
"""Create the subtitle path from the given video path using language if multi"""
if not os.path.exists(video_path):
path = os.path.splitext(os.path.basename(video_path))[0]
else:
path = os.path.splitext(video_path)[0]
if multi and language:
return path + '.%s%s' % (language, EXTENSIONS[0])
return path + '%s' % EXTENSIONS[0]
-66
View File
@@ -1,66 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
__all__ = ['Task', 'ListTask', 'DownloadTask', 'StopTask']
class Task(object):
"""Base class for tasks to use in subliminal"""
pass
class ListTask(Task):
"""List task used by the worker to search for subtitles
:param video: video to search subtitles for
:type video: :class:`~subliminal.videos.Video`
:param list languages: languages to search for
:param string service: name of the service to use
:param config: configuration for the service
:type config: :class:`~subliminal.services.ServiceConfig`
"""
def __init__(self, video, languages, service, config):
self.video = video
self.service = service
self.languages = languages
self.config = config
def __repr__(self):
return 'ListTask(%r, %r, %s, %r)' % (self.video, self.languages, self.service, self.config)
class DownloadTask(Task):
"""Download task used by the worker to download subtitles
:param video: video to download subtitles for
:type video: :class:`~subliminal.videos.Video`
:param subtitles: subtitles to download in order of preference
:type subtitles: list of :class:`~subliminal.subtitles.Subtitle`
"""
def __init__(self, video, subtitles):
self.video = video
self.subtitles = subtitles
def __repr__(self):
return 'DownloadTask(%r, %r)' % (self.video, self.subtitles)
class StopTask(Task):
"""Stop task that will stop the worker"""
pass
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from unittest import TextTestRunner, TestSuite
from subliminal import cache_region
from . import test_providers, test_subliminal
cache_region.configure('dogpile.cache.memory', expiration_time=60 * 30)
suite = TestSuite([test_providers.suite(), test_subliminal.suite()])
if __name__ == '__main__':
TextTestRunner().run(suite)
+19
View File
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from subliminal import Movie, Episode
MOVIES = [Movie('Man of Steel (2013)/man.of.steel.2013.720p.bluray.x264-felony.mkv', 'Man of Steel',
release_group='felony', resolution='720p', video_codec='h264', audio_codec='DTS', imdb_id=770828,
size=7033732714, year=2013,
hashes={'opensubtitles': '5b8f8f4e41ccb21e', 'thesubdb': 'ad32876133355929d814457537e12dc2'})]
EPISODES = [Episode('The Big Bang Theory/Season 07/The.Big.Bang.Theory.S07E05.720p.HDTV.X264-DIMENSION.mkv',
'The Big Bang Theory', 7, 5, release_group='DIMENSION', resolution='720p', video_codec='h264',
audio_codec='AC3', imdb_id=3229392, size=501910737, title='The Workplace Proximity',
tvdb_id=80379,
hashes={'opensubtitles': '6878b3ef7c1bd19e', 'thesubdb': '9dbbfb7ba81c9a6237237dae8589fccc'}),
Episode('Game of Thrones/Season 03/Game.of.Thrones.S03E10.Mhysa.720p.WEB-DL.DD5.1.H.264-NTb.mkv',
'Game of Thrones', 3, 10, release_group='NTb', resolution='720p', video_codec='h264',
audio_codec='AC3', imdb_id=2178796, size=2142810931, title='Mhysa', tvdb_id=121361,
hashes={'opensubtitles': 'b850baa096976c22', 'thesubdb': 'b1f899c77f4c960b84b8dbf840d4e42d'})]
+389
View File
@@ -0,0 +1,389 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
from unittest import TestCase, TestSuite, TestLoader, TextTestRunner
from babelfish import Language
from pkg_resources import iter_entry_points
from subliminal import PROVIDERS_ENTRY_POINT
from subliminal.subtitle import is_valid_subtitle
from subliminal.tests.common import MOVIES, EPISODES
class ProviderTestCase(TestCase):
provider_name = ''
def setUp(self):
for provider_entry_point in iter_entry_points(PROVIDERS_ENTRY_POINT, self.provider_name):
self.Provider = provider_entry_point.load()
break
class Addic7edProviderTestCase(ProviderTestCase):
provider_name = 'addic7ed'
def test_find_show_id(self):
with self.Provider() as provider:
show_id = provider.find_show_id('The Big Bang')
self.assertTrue(show_id == 126)
def test_find_show_id_error(self):
with self.Provider() as provider:
show_id = provider.find_show_id('the big how i met your mother')
self.assertTrue(show_id is None)
def test_get_show_ids(self):
with self.Provider() as provider:
show_ids = provider.get_show_ids()
self.assertTrue('the big bang theory' in show_ids and show_ids['the big bang theory'] == 126)
def test_query_episode_0(self):
video = EPISODES[0]
languages = {Language('tur'), Language('rus'), Language('heb'), Language('ita'), Language('fra'),
Language('ron'), Language('nld'), Language('eng'), Language('deu'), Language('ell'),
Language('por', 'BR'), Language('bul')}
matches = {frozenset(['episode', 'release_group', 'title', 'series', 'resolution', 'season']),
frozenset(['series', 'resolution', 'season']),
frozenset(['series', 'episode', 'season', 'title']),
frozenset(['series', 'release_group', 'season']),
frozenset(['series', 'episode', 'season', 'release_group', 'title']),
frozenset(['series', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1(self):
video = EPISODES[1]
languages = {Language('ind'), Language('spa'), Language('hrv'), Language('ita'), Language('fra'),
Language('cat'), Language('ell'), Language('nld'), Language('eng'), Language('fas'),
Language('por'), Language('nor'), Language('deu'), Language('ron'), Language('por', 'BR'),
Language('bul')}
matches = {frozenset(['series', 'episode', 'resolution', 'season', 'title']),
frozenset(['series', 'resolution', 'season']),
frozenset(['series', 'episode', 'season', 'title']),
frozenset(['series', 'release_group', 'season']),
frozenset(['series', 'resolution', 'release_group', 'season']),
frozenset(['series', 'episode', 'season', 'release_group', 'title']),
frozenset(['series', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = EPISODES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['series', 'episode', 'season', 'release_group', 'title']),
frozenset(['series', 'episode', 'season', 'title'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = EPISODES[0]
languages = {Language('eng'), Language('fra')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class BierDopjeProviderTestCase(ProviderTestCase):
provider_name = 'bierdopje'
def test_find_show_id(self):
with self.Provider() as provider:
show_id = provider.find_show_id('The Big Bang')
self.assertTrue(show_id == 9203)
def test_find_show_id_error(self):
with self.Provider() as provider:
show_id = provider.find_show_id('the big how i met your mother')
self.assertTrue(show_id is None)
def test_query_episode_0(self):
video = EPISODES[0]
language = Language('eng')
matches = {frozenset(['series', 'video_codec', 'resolution', 'episode', 'season']),
frozenset(['season', 'video_codec', 'episode', 'series']),
frozenset(['episode', 'video_codec', 'season', 'series', 'resolution', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.query(language, video.season, video.episode, series=video.series)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_query_episode_1(self):
video = EPISODES[1]
language = Language('nld')
matches = {frozenset(['series', 'video_codec', 'resolution', 'episode', 'season']),
frozenset(['season', 'video_codec', 'episode', 'series']),
frozenset(['series', 'episode', 'season']),
frozenset(['season', 'video_codec', 'episode', 'release_group', 'series']),
frozenset(['episode', 'video_codec', 'season', 'series', 'resolution', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.query(language, video.season, video.episode, series=video.series)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_query_episode_0_tvdb_id(self):
video = EPISODES[0]
language = Language('eng')
matches = {frozenset(['video_codec', 'tvdb_id', 'episode', 'season', 'series']),
frozenset(['episode', 'video_codec', 'series', 'season', 'tvdb_id', 'resolution', 'release_group']),
frozenset(['episode', 'series', 'video_codec', 'tvdb_id', 'resolution', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(language, video.season, video.episode, tvdb_id=video.tvdb_id)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_list_subtitles(self):
video = EPISODES[1]
languages = {Language('eng'), Language('nld')}
matches = {frozenset(['series', 'video_codec', 'tvdb_id', 'episode', 'season']),
frozenset(['episode', 'video_codec', 'season', 'series', 'tvdb_id', 'resolution', 'release_group']),
frozenset(['season', 'tvdb_id', 'episode', 'series']),
frozenset(['episode', 'video_codec', 'season', 'series', 'tvdb_id', 'resolution']),
frozenset(['episode', 'video_codec', 'season', 'series', 'tvdb_id', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = EPISODES[0]
languages = {Language('eng'), Language('nld')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class OpenSubtitlesProviderTestCase(ProviderTestCase):
provider_name = 'opensubtitles'
def test_query_movie_0_query(self):
video = MOVIES[0]
languages = {Language('eng')}
matches = {frozenset([]), frozenset(['imdb_id', 'resolution', 'title', 'year']),
frozenset(['imdb_id', 'title', 'year']),
frozenset(['imdb_id', 'video_codec', 'title', 'year']),
frozenset(['imdb_id', 'resolution', 'title', 'video_codec', 'year']),
frozenset(['imdb_id', 'title', 'year', 'video_codec', 'resolution', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.query(languages, query=video.title)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_0_query(self):
video = EPISODES[0]
languages = {Language('eng')}
matches = {frozenset(['series', 'episode', 'season', 'imdb_id']),
frozenset(['series', 'imdb_id', 'video_codec', 'episode', 'season']),
frozenset(['episode', 'title', 'series', 'imdb_id', 'video_codec', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(languages, query=video.name.split(os.sep)[-1])
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1_query(self):
video = EPISODES[1]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['episode', 'title', 'series', 'imdb_id', 'video_codec', 'season']),
frozenset(['series', 'imdb_id', 'title', 'episode', 'season']),
frozenset(['series', 'imdb_id', 'video_codec', 'episode', 'season']),
frozenset(['episode', 'video_codec', 'series', 'imdb_id', 'resolution', 'season']),
frozenset(['series', 'imdb_id', 'resolution', 'episode', 'season']),
frozenset(['series', 'episode', 'season', 'imdb_id'])}
with self.Provider() as provider:
subtitles = provider.query(languages, query=video.name.split(os.sep)[-1])
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_movie_0_imdb_id(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['imdb_id', 'video_codec', 'title', 'year']),
frozenset(['imdb_id', 'resolution', 'title', 'video_codec', 'year']),
frozenset(['imdb_id', 'title', 'year', 'video_codec', 'resolution', 'release_group']),
frozenset(['imdb_id', 'title', 'year']),
frozenset(['imdb_id', 'resolution', 'title', 'year'])}
with self.Provider() as provider:
subtitles = provider.query(languages, imdb_id=video.imdb_id)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_0_imdb_id(self):
video = EPISODES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['series', 'episode', 'season', 'imdb_id']),
frozenset(['episode', 'release_group', 'video_codec', 'series', 'imdb_id', 'resolution', 'season']),
frozenset(['series', 'imdb_id', 'video_codec', 'episode', 'season']),
frozenset(['episode', 'title', 'series', 'imdb_id', 'video_codec', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(languages, imdb_id=video.imdb_id)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_movie_0_hash(self):
video = MOVIES[0]
languages = {Language('eng')}
matches = {frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'imdb_id']),
frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'release_group', 'imdb_id']),
frozenset(['year', 'video_codec', 'imdb_id', 'hash', 'title']),
frozenset(['year', 'resolution', 'imdb_id', 'hash', 'title']),
frozenset(['year', 'imdb_id', 'hash', 'title'])}
with self.Provider() as provider:
subtitles = provider.query(languages, hash=video.hashes['opensubtitles'], size=video.size)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_0_hash(self):
video = EPISODES[0]
languages = {Language('eng')}
matches = {frozenset(['series', 'hash']),
frozenset(['episode', 'season', 'series', 'imdb_id', 'video_codec', 'hash']),
frozenset(['series', 'episode', 'season', 'hash', 'imdb_id']),
frozenset(['series', 'resolution', 'hash', 'video_codec'])}
with self.Provider() as provider:
subtitles = provider.query(languages, hash=video.hashes['opensubtitles'], size=video.size)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['title', 'video_codec', 'year', 'resolution', 'release_group', 'imdb_id']),
frozenset(['imdb_id', 'year', 'title']),
frozenset(['year', 'video_codec', 'imdb_id', 'resolution', 'title']),
frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'release_group', 'imdb_id']),
frozenset(['year', 'video_codec', 'imdb_id', 'hash', 'title']),
frozenset(['year', 'resolution', 'imdb_id', 'hash', 'title']),
frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'imdb_id']),
frozenset(['year', 'imdb_id', 'hash', 'title']),
frozenset(['video_codec', 'imdb_id', 'year', 'title']),
frozenset(['year', 'imdb_id', 'resolution', 'title'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class TheSubDBProviderTestCase(ProviderTestCase):
provider_name = 'thesubdb'
def test_query_episode_0(self):
video = EPISODES[0]
languages = {Language('eng'), Language('spa'), Language('por')}
matches = {frozenset(['hash'])}
with self.Provider() as provider:
subtitles = provider.query(video.hashes['thesubdb'])
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1(self):
video = EPISODES[1]
languages = {Language('eng'), Language('por')}
matches = {frozenset(['hash'])}
with self.Provider() as provider:
subtitles = provider.query(video.hashes['thesubdb'])
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = MOVIES[0]
languages = {Language('eng'), Language('por')}
matches = {frozenset(['hash'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = MOVIES[0]
languages = {Language('eng'), Language('por')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class TVsubtitlesProviderTestCase(ProviderTestCase):
provider_name = 'tvsubtitles'
def test_find_show_id(self):
with self.Provider() as provider:
show_id = provider.find_show_id('The Big Bang')
self.assertTrue(show_id == 154)
def test_find_show_id_error(self):
with self.Provider() as provider:
show_id = provider.find_show_id('the big gaming')
self.assertTrue(show_id is None)
def test_find_episode_ids(self):
with self.Provider() as provider:
episode_ids = provider.find_episode_ids(154, 5)
self.assertTrue(set(episode_ids.keys()) == set(range(1, 25)))
def test_query_episode_0(self):
video = EPISODES[0]
languages = {Language('fra'), Language('por'), Language('hun'), Language('ron'), Language('eng')}
matches = {frozenset(['series', 'episode', 'season', 'video_codec']),
frozenset(['series', 'episode', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season, video.episode)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1(self):
video = EPISODES[1]
languages = {Language('fra'), Language('ell'), Language('ron'), Language('eng'), Language('hun'),
Language('por'), Language('por', 'BR')}
matches = {frozenset(['series', 'episode', 'resolution', 'season']),
frozenset(['series', 'episode', 'season', 'video_codec']),
frozenset(['series', 'episode', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season, video.episode)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = EPISODES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['series', 'episode', 'season'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = EPISODES[0]
languages = {Language('hun')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
def suite():
suite = TestSuite()
suite.addTest(TestLoader().loadTestsFromTestCase(Addic7edProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(BierDopjeProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(OpenSubtitlesProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(TheSubDBProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(TVsubtitlesProviderTestCase))
return suite
if __name__ == '__main__':
TextTestRunner().run(suite())
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import shutil
from unittest import TestCase, TestSuite, TestLoader, TextTestRunner
from babelfish import Language
from subliminal import list_subtitles, download_subtitles, download_best_subtitles, scan_video, scan_videos
from subliminal.tests.common import MOVIES, EPISODES
TEST_DIR = 'test_data'
class ApiTestCase(TestCase):
def setUp(self):
os.mkdir(TEST_DIR)
def tearDown(self):
shutil.rmtree(TEST_DIR)
def test_list_subtitles_movie_0(self):
videos = [MOVIES[0]]
languages = {Language('eng')}
subtitles = list_subtitles(videos, languages)
self.assertTrue(len(subtitles) == len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_list_subtitles_movie_0_por_br(self):
videos = [MOVIES[0]]
languages = {Language('por', 'BR')}
subtitles = list_subtitles(videos, languages)
self.assertTrue(len(subtitles) == len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_list_subtitles_episodes(self):
videos = [EPISODES[0], EPISODES[1]]
languages = {Language('eng'), Language('fra')}
subtitles = list_subtitles(videos, languages)
self.assertTrue(len(subtitles) == len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_download_subtitles(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = list_subtitles(videos, languages)
download_subtitles(subtitles)
for video in videos:
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.en.srt'))
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.fr.srt'))
def test_download_subtitles_single(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = list_subtitles(videos, languages)
download_subtitles(subtitles, single=True)
for video in videos:
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.srt'))
def test_download_best_subtitles(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = download_best_subtitles(videos, languages)
for video in videos:
self.assertTrue(video in subtitles and len(subtitles[video]) == 2)
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.en.srt'))
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.fr.srt'))
def test_download_best_subtitles_single(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = download_best_subtitles(videos, languages, single=True)
for video in videos:
self.assertTrue(video in subtitles and len(subtitles[video]) == 1)
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.srt'))
def test_download_best_subtitles_min_score(self):
videos = [MOVIES[0]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = download_best_subtitles(videos, languages, min_score=1000)
self.assertTrue(len(subtitles) == 0)
def test_download_best_subtitles_hearing_impaired(self):
videos = [MOVIES[0]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng')}
subtitles = download_best_subtitles(videos, languages, hearing_impaired=True)
self.assertTrue(subtitles[videos[0]][0].hearing_impaired == True)
class VideoTestCase(TestCase):
def setUp(self):
os.mkdir(TEST_DIR)
for video in MOVIES + EPISODES:
open(os.path.join(TEST_DIR, os.path.split(video.name)[1]), 'w').close()
def tearDown(self):
shutil.rmtree(TEST_DIR)
def test_scan_video_movie(self):
video = MOVIES[0]
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.name == os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.title.lower() == video.title.lower())
self.assertTrue(scanned_video.year == video.year)
self.assertTrue(scanned_video.video_codec == video.video_codec)
self.assertTrue(scanned_video.resolution == video.resolution)
self.assertTrue(scanned_video.release_group == video.release_group)
self.assertTrue(scanned_video.subtitle_languages == set())
self.assertTrue(scanned_video.hashes == {})
self.assertTrue(scanned_video.audio_codec is None)
self.assertTrue(scanned_video.imdb_id is None)
self.assertTrue(scanned_video.size == 0)
def test_scan_video_episode(self):
video = EPISODES[0]
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.name == os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.series == video.series)
self.assertTrue(scanned_video.season == video.season)
self.assertTrue(scanned_video.episode == video.episode)
self.assertTrue(scanned_video.video_codec == video.video_codec)
self.assertTrue(scanned_video.resolution == video.resolution)
self.assertTrue(scanned_video.release_group == video.release_group)
self.assertTrue(scanned_video.subtitle_languages == set())
self.assertTrue(scanned_video.hashes == {})
self.assertTrue(scanned_video.title is None)
self.assertTrue(scanned_video.tvdb_id is None)
self.assertTrue(scanned_video.imdb_id is None)
self.assertTrue(scanned_video.audio_codec is None)
self.assertTrue(scanned_video.size == 0)
def test_scan_video_subtitle_language_und(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.subtitle_languages == {Language('und')})
def test_scan_video_subtitles_language_eng(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.en.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.subtitle_languages == {Language('eng')})
def test_scan_video_subtitles_languages(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.en.srt', 'w').close()
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.fr.srt', 'w').close()
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.subtitle_languages == {Language('eng'), Language('fra'), Language('und')})
def suite():
suite = TestSuite()
suite.addTest(TestLoader().loadTestsFromTestCase(ApiTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(VideoTestCase))
return suite
if __name__ == '__main__':
TextTestRunner().run(suite())
-64
View File
@@ -1,64 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
import re
__all__ = ['get_keywords', 'split_keyword', 'to_unicode']
def get_keywords(guess):
"""Retrieve keywords from guessed informations
:param guess: guessed informations
:type guess: :class:`guessit.guess.Guess`
:return: lower case alphanumeric keywords
:rtype: set
"""
keywords = set()
for k in ['releaseGroup', 'screenSize', 'videoCodec', 'format']:
if k in guess:
keywords = keywords | split_keyword(guess[k].lower())
return keywords
def split_keyword(keyword):
"""Split a keyword in multiple ones on any non-alphanumeric character
:param string keyword: keyword
:return: keywords
:rtype: set
"""
split = set(re.findall(r'\w+', keyword))
return split
def to_unicode(data):
"""Convert a basestring to unicode
:param basestring data: data to decode
:return: data as unicode
:rtype: unicode
"""
if not isinstance(data, basestring):
raise ValueError('Basestring expected')
if isinstance(data, unicode):
return data
return unicode(data, 'utf-8', 'replace')
+371
View File
@@ -0,0 +1,371 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import datetime
import hashlib
import logging
import os
import struct
import babelfish
import enzyme
import guessit
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, with various properties that defines it.
Each property has an associated score based on equations that are described in
subclasses.
:param string name: name or path of the video
:param string release_group: release group of the video
:param string resolution: screen size of the video stream (480p, 720p, 1080p or 1080i)
:param string video_codec: codec of the video stream
:param string 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: byte size of the video file
:param set subtitle_languages: existing subtitle languages
"""
scores = {}
def __init__(self, name, release_group=None, resolution=None, video_codec=None, audio_codec=None, imdb_id=None,
hashes=None, size=None, subtitle_languages=None):
self.name = name
self.release_group = release_group
self.resolution = resolution
self.video_codec = video_codec
self.audio_codec = audio_codec
self.imdb_id = imdb_id
self.hashes = hashes or {}
self.size = size
self.subtitle_languages = subtitle_languages or set()
@classmethod
def fromguess(cls, name, guess):
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')
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.get_episode_equations`
:param string series: series of the episode
:param int season: season number of the episode
:param int episode: episode number of the episode
:param string title: title of the episode
:param int tvdb_id: TheTVDB id of the episode
"""
scores = {'title': 12, 'video_codec': 2, 'imdb_id': 35, 'audio_codec': 1, 'tvdb_id': 23, 'resolution': 2,
'season': 6, 'release_group': 6, 'series': 23, 'episode': 6, 'hash': 46}
def __init__(self, name, series, season, episode, release_group=None, resolution=None, video_codec=None,
audio_codec=None, imdb_id=None, hashes=None, size=None, subtitle_languages=None, title=None,
tvdb_id=None):
super(Episode, self).__init__(name, release_group, resolution, video_codec, audio_codec, imdb_id, hashes,
size, subtitle_languages)
self.series = series
self.season = season
self.episode = episode
self.title = title
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'],
release_group=guess.get('releaseGroup'), resolution=guess.get('screenSize'),
video_codec=guess.get('videoCodec'), audio_codec=guess.get('audioCodec'),
title=guess.get('title'))
def __repr__(self):
return '<%s [%r, %rx%r]>' % (self.__class__.__name__, self.series, self.season, self.episode)
class Movie(Video):
"""Movie :class:`Video`
Scores are defined by a set of equations, see :func:`~subliminal.score.get_movie_equations`
:param string title: title of the movie
:param int year: year of the movie
"""
scores = {'title': 13, 'video_codec': 2, 'resolution': 2, 'audio_codec': 1, 'year': 7, 'imdb_id': 31,
'release_group': 6, 'hash': 31}
def __init__(self, name, title, 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, release_group, resolution, video_codec, audio_codec, imdb_id, hashes,
size, subtitle_languages)
self.title = title
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'], release_group=guess.get('releaseGroup'), resolution=guess.get('screenSize'),
video_codec=guess.get('videoCodec'), audio_codec=guess.get('audioCodec'),
year=guess.get('year'))
def __repr__(self):
if self.year is None:
return '<%s [%r]>' % (self.__class__.__name__, self.title)
return '<%s [%r, %r]>' % (self.__class__.__name__, self.title, self.year)
def scan_subtitle_languages(path):
"""Search for subtitles with alpha2 extension from a video `path` and return their language
:param string path: path to the video
:return: found subtitle languages
:rtype: set
"""
language_extensions = tuple('.' + c for c in babelfish.CONVERTERS['alpha2'].codes)
dirpath, filename = os.path.split(path)
subtitles = set()
for p in os.listdir(dirpath):
if not isinstance(p, bytes) and p.startswith(os.path.splitext(filename)[0]) and p.endswith(SUBTITLE_EXTENSIONS):
if os.path.splitext(p)[0].endswith(language_extensions):
subtitles.add(babelfish.Language.fromalpha2(os.path.splitext(p)[0][-2:]))
else:
subtitles.add(babelfish.Language('und'))
logger.debug('Found subtitles %r', subtitles)
return subtitles
def scan_video(path, subtitles=True, embedded_subtitles=True):
"""Scan a video and its subtitle languages from a video `path`
:param string path: absolute path to the video
:param bool subtitles: scan for subtitles with the same name
:param bool embedded_subtitles: scan for embedded subtitles
:return: the scanned video
:rtype: :class:`Video`
:raise: ValueError if cannot guess enough information from the path
"""
dirpath, filename = os.path.split(path)
logger.info('Scanning video %r in %r', filename, dirpath)
video = Video.fromguess(path, guessit.guess_file_info(path, 'autodetect'))
video.size = os.path.getsize(path)
if video.size > 10485760:
logger.debug('Size is %d', video.size)
video.hashes['opensubtitles'] = hash_opensubtitles(path)
video.hashes['thesubdb'] = hash_thesubdb(path)
logger.debug('Computed hashes %r', video.hashes)
else:
logger.warning('Size is lower than 10MB: hashes not computed')
if subtitles:
video.subtitle_languages |= scan_subtitle_languages(path)
# enzyme
try:
if filename.endswith('.mkv'):
with open(path, 'rb') as f:
mkv = enzyme.MKV(f)
if mkv.video_tracks:
video_track = mkv.video_tracks[0]
# resolution
if video_track.height in (480, 720, 1080):
if video_track.interlaced:
video.resolution = '%di' % video_track.height
logger.debug('Found resolution %s with enzyme', video.resolution)
else:
video.resolution = '%dp' % video_track.height
logger.debug('Found resolution %s with enzyme', video.resolution)
# video codec
if video_track.codec_id == 'V_MPEG4/ISO/AVC':
video.video_codec = 'h264'
logger.debug('Found video_codec %s with enzyme', video.video_codec)
elif video_track.codec_id == 'V_MPEG4/ISO/SP':
video.video_codec = 'DivX'
logger.debug('Found video_codec %s with enzyme', video.video_codec)
elif video_track.codec_id == 'V_MPEG4/ISO/ASP':
video.video_codec = 'XviD'
logger.debug('Found video_codec %s with enzyme', video.video_codec)
else:
logger.warning('MKV has no video track')
if mkv.audio_tracks:
audio_track = mkv.audio_tracks[0]
# audio codec
if audio_track.codec_id == 'A_AC3':
video.audio_codec = 'AC3'
logger.debug('Found audio_codec %s with enzyme', video.audio_codec)
elif audio_track.codec_id == 'A_DTS':
video.audio_codec = 'DTS'
logger.debug('Found audio_codec %s with enzyme', video.audio_codec)
elif audio_track.codec_id == 'A_AAC':
video.audio_codec = 'AAC'
logger.debug('Found audio_codec %s with enzyme', video.audio_codec)
else:
logger.warning('MKV has no audio track')
if mkv.subtitle_tracks:
# embedded subtitles
if embedded_subtitles:
embedded_subtitle_languages = set()
for st in mkv.subtitle_tracks:
try:
embedded_subtitle_languages.add(babelfish.Language.fromalpha3b(st.language or 'und'))
except babelfish.Error:
logger.error('Embedded subtitle language %r is not a valid language', st.language)
logger.debug('Found embedded subtitle %r with enzyme', embedded_subtitle_languages)
video.subtitle_languages |= embedded_subtitle_languages
else:
logger.info('MKV has no subtitle track')
except enzyme.Error:
logger.error('Parsing video metadata with enzyme failed')
return video
def scan_videos(paths, subtitles=True, embedded_subtitles=True, age=None):
"""Scan `paths` for videos and their subtitle languages
:params paths: absolute paths to scan for videos
:type paths: list of string
:param bool subtitles: scan for subtitles with the same name
:param bool embedded_subtitles: scan for embedded subtitles
:param age: age of the video, if any
:type age: datetime.timedelta or None
:return: the scanned videos
:rtype: list of :class:`Video`
"""
videos = []
# scan files
for filepath in [p for p in paths if os.path.isfile(p)]:
if age and datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(filepath)) > age:
logger.info('Skipping video %r: older than %r', filepath, age)
continue
try:
videos.append(scan_video(filepath, subtitles, embedded_subtitles))
except ValueError as e:
logger.error('Skipping video: %s', e)
continue
# scan directories
for path in [p for p in paths if os.path.isdir(p)]:
logger.info('Scanning directory %r', path)
for dirpath, dirnames, filenames in os.walk(path):
# skip badly encoded directories
if isinstance(dirpath, bytes):
logger.error('Skipping badly encoded directory %r', dirpath.decode('utf-8', errors='replace'))
continue
# skip badly encoded and hidden sub directories
for dirname in list(dirnames):
if isinstance(dirname, bytes):
logger.error('Skipping badly encoded dirname %r in %r', dirname.decode('utf-8', errors='replace'),
dirpath)
dirnames.remove(dirname)
elif dirname.startswith('.'):
logger.debug('Skipping hidden dirname %r in %r', dirname, dirpath)
dirnames.remove(dirname)
# scan for videos
for filename in filenames:
# skip badly encoded files
if isinstance(filename, bytes):
logger.error('Skipping badly encoded filename %r in %r', filename.decode('utf-8', errors='replace'),
dirpath)
continue
# filter videos
if not filename.endswith(VIDEO_EXTENSIONS):
continue
# skip hidden files
if filename.startswith('.'):
logger.debug('Skipping hidden filename %r in %r', filename, dirpath)
continue
filepath = os.path.join(dirpath, filename)
# skip links
if os.path.islink(filepath):
logger.debug('Skipping link %r in %r', filename, dirpath)
continue
if age and datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(filepath)) > age:
logger.info('Skipping video %r: older than %r', filepath, age)
continue
try:
video = scan_video(filepath, subtitles, embedded_subtitles)
except ValueError as e:
logger.error('Skipping video: %s', e)
continue
videos.append(video)
return videos
def hash_opensubtitles(video_path):
"""Compute a hash using OpenSubtitles' algorithm
:param string video_path: path of the video
:return: the hash
:rtype: string
"""
bytesize = struct.calcsize(b'q')
with open(video_path, 'rb') as f:
filesize = os.path.getsize(video_path)
filehash = filesize
if filesize < 65536 * 2:
return None
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(b'q', filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF # to remain as 64bit number
f.seek(max(0, filesize - 65536), 0)
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(b'q', filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF
returnedhash = '%016x' % filehash
return returnedhash
def hash_thesubdb(video_path):
"""Compute a hash using TheSubDB's algorithm
:param string video_path: path of the video
:return: the hash
:rtype: string
"""
readsize = 64 * 1024
if os.path.getsize(video_path) < readsize:
return None
with open(video_path, 'rb') as f:
data = f.read(readsize)
f.seek(-readsize, os.SEEK_END)
data += f.read(readsize)
return hashlib.md5(data).hexdigest().decode('ascii')
-269
View File
@@ -1,269 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import subtitles
from .languages import list_languages
import enzyme
import guessit
import hashlib
import logging
import mimetypes
import os
import struct
__all__ = ['EXTENSIONS', 'MIMETYPES', 'Video', 'Episode', 'Movie', 'UnknownVideo',
'scan', 'hash_opensubtitles', 'hash_thesubdb']
logger = logging.getLogger(__name__)
#: Video extensions
EXTENSIONS = ['.avi', '.mkv', '.mpg', '.mp4', '.m4v', '.mov', '.ogm', '.ogv', '.wmv',
'.divx', '.asf']
#: Video mimetypes
MIMETYPES = ['video/mpeg', 'video/mp4', 'video/quicktime', 'video/x-ms-wmv', 'video/x-msvideo',
'video/x-flv', 'video/x-matroska', 'video/x-matroska-3d']
class Video(object):
"""Base class for videos
:param string path: path
:param guess: guessed informations
:type guess: :class:`~guessit.guess.Guess`
:param string imdbid: imdbid
"""
def __init__(self, path, guess, imdbid=None):
self.release = path
self.guess = guess
self.imdbid = imdbid
self._path = None
self.hashes = {}
if os.path.exists(path):
self._path = path
self.size = os.path.getsize(self._path)
self._compute_hashes()
@classmethod
def from_path(cls, path):
"""Create a :class:`Video` subclass guessing all informations from the given path
:param string path: path
:return: video object
:rtype: :class:`Episode` or :class:`Movie` or :class:`UnknownVideo`
"""
guess = guessit.guess_file_info(path, 'autodetect')
result = None
if guess['type'] == 'episode' and 'series' in guess and 'season' in guess and 'episodeNumber' in guess:
title = None
if 'title' in guess:
title = guess['title']
result = Episode(path, guess['series'], guess['season'], guess['episodeNumber'], title, guess)
if guess['type'] == 'movie' and 'title' in guess:
year = None
if 'year' in guess:
year = guess['year']
result = Movie(path, guess['title'], year, guess)
if not result:
result = UnknownVideo(path, guess)
if not isinstance(result, cls):
raise ValueError('Video is not of requested type')
return result
@property
def exists(self):
"""Whether the video exists or not"""
if self._path:
return os.path.exists(self._path)
return False
@property
def path(self):
"""Path to the video"""
return self._path
@path.setter
def path(self, value):
if not os.path.exists(value):
raise ValueError('Path does not exists')
self._path = value
self.size = os.path.getsize(self._path)
self._compute_hashes()
def _compute_hashes(self):
"""Compute different hashes"""
self.hashes['OpenSubtitles'] = hash_opensubtitles(self.path)
self.hashes['TheSubDB'] = hash_thesubdb(self.path)
def scan(self):
"""Scan and return associated subtitles
:return: associated subtitles
:rtype: list of :class:`~subliminal.subtitles.Subtitle`
"""
if not self.exists:
return []
basepath = os.path.splitext(self.path)[0]
results = []
video_infos = None
try:
video_infos = enzyme.parse(self.path)
logger.debug(u'Succeeded parsing %s with enzyme: %r' % (self.path, video_infos))
except:
logger.debug(u'Failed parsing %s with enzyme' % self.path)
if isinstance(video_infos, enzyme.core.AVContainer):
results.extend([subtitles.EmbeddedSubtitle.from_enzyme(self.path, s) for s in video_infos.subtitles])
for l in list_languages(1):
for e in subtitles.EXTENSIONS:
single_path = basepath + '%s' % e
if os.path.exists(single_path):
results.append(subtitles.ExternalSubtitle(single_path, None))
multi_path = basepath + '.%s%s' % (l, e)
if os.path.exists(multi_path):
results.append(subtitles.ExternalSubtitle(multi_path, l))
return results
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.release)
class Episode(Video):
"""Episode :class:`Video`
:param string path: path
:param string series: series
:param int season: season number
:param int episode: episode number
:param string title: title
:param guess: guessed informations
:type guess: :class:`~guessit.guess.Guess`
:param string tvdbid: tvdbid
:param string imdbid: imdbid
"""
def __init__(self, path, series, season, episode, title=None, guess=None, tvdbid=None, imdbid=None):
super(Episode, self).__init__(path, guess, imdbid)
self.series = series
self.title = title
self.season = season
self.episode = episode
self.tvdbid = tvdbid
class Movie(Video):
"""Movie :class:`Video`
:param string path: path
:param string title: title
:param int year: year
:param guess: guessed informations
:type guess: :class:`~guessit.guess.Guess`
:param string imdbid: imdbid
"""
def __init__(self, path, title, year=None, guess=None, imdbid=None):
super(Movie, self).__init__(path, guess, imdbid)
self.title = title
self.year = year
class UnknownVideo(Video):
"""Unknown video"""
pass
def scan(entry, max_depth=3, depth=0):
"""Scan a path for videos and subtitles
:param string entry: path
:param int max_depth: maximum folder depth
:param int depth: starting depth
:return: found videos and subtitles
:rtype: list of (:class:`Video`, [:class:`~subliminal.subtitles.Subtitle`])
"""
if depth > max_depth and max_depth != 0: # we do not want to search the whole file system except if max_depth = 0
return []
if depth == 0:
entry = os.path.abspath(entry)
if os.path.isdir(entry): # a dir? recurse
logger.debug(u'Scanning directory %s with depth %d/%d' % (entry, depth, max_depth))
result = []
for e in os.listdir(entry):
result.extend(scan(os.path.join(entry, e), max_depth, depth + 1))
return result
if os.path.isfile(entry) or depth == 0:
logger.debug(u'Scanning file %s with depth %d/%d' % (entry, depth, max_depth))
if depth != 0: # trust the user: only check for valid format if recursing
if mimetypes.guess_type(entry)[0] not in MIMETYPES and os.path.splitext(entry)[1] not in EXTENSIONS:
return []
video = Video.from_path(entry)
return [(video, video.scan())]
logger.warning(u'Scanning entry %s failed with depth %d/%d' % (entry, depth, max_depth))
return [] # anything else
def hash_opensubtitles(path):
"""Compute a hash using OpenSubtitles' algorithm
:param string path: path
:return: hash
:rtype: string
"""
longlongformat = 'q' # long long
bytesize = struct.calcsize(longlongformat)
with open(path, 'rb') as f:
filesize = os.path.getsize(path)
filehash = filesize
if filesize < 65536 * 2:
return None
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(longlongformat, filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF # to remain as 64bit number
f.seek(max(0, filesize - 65536), 0)
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(longlongformat, filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF
returnedhash = '%016x' % filehash
logger.debug(u'Computed OpenSubtitle hash %s for %s' % (returnedhash, path))
return returnedhash
def hash_thesubdb(path):
"""Compute a hash using TheSubDB's algorithm
:param string path: path
:return: hash
:rtype: string
"""
readsize = 64 * 1024
with open(path, 'rb') as f:
data = f.read(readsize)
f.seek(-readsize, os.SEEK_END)
data += f.read(readsize)
returnedhash = hashlib.md5(data).hexdigest()
logger.debug(u'Computed TheSubDB hash %s for %s' % (returnedhash, path))
return returnedhash
-58
View File
@@ -1,58 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from subliminal import Pool
import os
import time
import unittest
cache_dir = u'/tmp/sublicache'
if not os.path.exists(cache_dir):
os.mkdir(cache_dir)
existing_video = u'/something/The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4'
class AsyncTestCase(unittest.TestCase):
def test_pool(self):
p = Pool(4)
self.assertTrue(len(p.workers) == 4)
for w in p.workers:
self.assertTrue(w.isAlive() == False)
p.start()
for w in p.workers:
self.assertTrue(w.isAlive() == True)
p.stop()
p.join()
time.sleep(0.2) # so terminate is finished on Worker and proper Thread methods finished
for w in p.workers:
self.assertTrue(w.isAlive() == False)
def test_list_subtitles(self):
with Pool(4) as p:
results = p.list_subtitles(existing_video, languages=['en', 'fr'], cache_dir=cache_dir, max_depth=3)
self.assertTrue(len(results) > 0)
def test_download_subtitles(self):
with Pool(4) as p:
results = p.download_subtitles(existing_video, languages=['en', 'fr'], cache_dir=cache_dir, max_depth=3)
self.assertTrue(len(results) > 0)
if __name__ == '__main__':
unittest.main()
-392
View File
@@ -1,392 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from subliminal import videos
from subliminal.exceptions import MissingLanguageError, ServiceError
from subliminal.services import ServiceConfig
from subliminal.services.bierdopje import BierDopje
from subliminal.services.opensubtitles import OpenSubtitles
from subliminal.services.subswiki import SubsWiki
from subliminal.services.subtitulos import Subtitulos
from subliminal.services.thesubdb import TheSubDB
from subliminal.subtitles import Subtitle
import os
import unittest
cache_dir = u'/tmp/sublicache'
if not os.path.exists(cache_dir):
os.mkdir(cache_dir)
existing_video = u'/something/The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4'
class BierDopjeTestCase(unittest.TestCase):
query_tests = ['test_query_series', 'test_query_wrong_series', 'test_query_wrong_languages',
'test_query_tvdbid', 'test_query_wrong_tvdbid', 'test_query_series_and_tvdbid']
list_tests = ['test_list_episode', 'test_list_movie', 'test_list_wrong_languages']
download_tests = ['test_download']
def setUp(self):
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.episode_path = u'The Big Bang Theory/Season 05/S05E06 - The Rhinitis Revelation - HD TV.mkv'
self.movie_path = u'Inception (2010)/Inception - 1080p.mkv'
self.languages = set(['en', 'nl'])
self.wrong_languages = set(['zz', 'es'])
self.fake_file = u'/tmp/fake_file'
self.series = 'The Big Bang Theory'
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
self.tvdbid = 80379
self.wrong_tvdbid = 9999999999
def test_query_series(self):
with BierDopje(self.config) as service:
results = service.query(self.season, self.episode, self.languages, self.fake_file, series=self.series)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with BierDopje(self.config) as service:
results = service.query(self.season, self.episode, self.languages, self.fake_file, series=self.wrong_series)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with BierDopje(self.config) as service:
results = service.query(self.season, self.episode, self.wrong_languages, self.fake_file, series=self.series)
self.assertTrue(len(results) == 0)
def test_query_tvdbid(self):
with BierDopje(self.config) as service:
results = service.query(self.season, self.episode, self.languages, self.fake_file, tvdbid=self.tvdbid)
self.assertTrue(len(results) > 0)
def test_query_series_and_tvdbid(self):
with BierDopje(self.config) as service:
results = service.query(self.season, self.episode, self.languages, self.fake_file, series=self.series, tvdbid=self.tvdbid)
self.assertTrue(len(results) > 0)
def test_query_wrong_tvdbid(self):
with BierDopje(self.config) as service:
results = service.query(self.season, self.episode, self.languages, self.fake_file, tvdbid=self.wrong_tvdbid)
self.assertTrue(len(results) == 0)
def test_list_episode(self):
episode = videos.Video.from_path(self.episode_path)
with BierDopje(self.config) as service:
results = service.list(episode, self.languages)
self.assertTrue(len(results) > 0)
def test_list_movie(self):
movie = videos.Video.from_path(self.movie_path)
with BierDopje(self.config) as service:
results = service.list(movie, self.languages)
self.assertTrue(len(results) == 0)
def test_list_wrong_languages(self):
episode = videos.Video.from_path(self.episode_path)
with BierDopje(self.config) as service:
results = service.list(episode, self.wrong_languages)
self.assertTrue(len(results) == 0)
def test_download(self):
episode = videos.Video.from_path(self.episode_path)
with BierDopje(self.config) as service:
subtitle = service.list(episode, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
service.download(subtitle)
self.assertTrue(os.path.exists(subtitle.path))
class OpenSubtitlesTestCase(unittest.TestCase):
query_tests = ['test_query_query', 'test_query_imdbid', 'test_query_hash', 'test_query_wrong_languages']
list_tests = ['test_list', 'test_list_wrong_languages']
download_tests = ['test_download']
def setUp(self):
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.languages = set(['en', 'fr'])
self.wrong_languages = set(['zz', 'yy'])
self.fake_file = u'/tmp/fake_file'
self.path = existing_video
self.series = 'The Big Bang Theory'
self.movie = 'Inception'
self.wrong_series = 'No Existent Show Name'
self.imdbid = 'tt1375666'
self.wrong_imdbid = 'tt9999999'
self.hash = '51e57c4e8fd77990'
self.size = 882571264L
def test_query_query(self):
with OpenSubtitles(self.config) as service:
results = service.query(self.fake_file, self.languages, query=self.movie)
self.assertTrue(len(results) > 0)
def test_query_imdbid(self):
with OpenSubtitles(self.config) as service:
results = service.query(self.fake_file, self.languages, imdbid=self.imdbid)
self.assertTrue(len(results) > 0)
def test_query_hash(self):
with OpenSubtitles(self.config) as service:
results = service.query(self.fake_file, self.languages, moviehash=self.hash, size=self.size)
self.assertTrue(len(results) > 0)
def test_query_wrong_languages(self):
with OpenSubtitles(self.config) as service:
with self.assertRaises(MissingLanguageError):
service.query(self.fake_file, self.wrong_languages, moviehash=self.hash, size=self.size)
def test_list(self):
video = videos.Video.from_path(self.path)
with OpenSubtitles(self.config) as service:
results = service.list(video, self.languages)
self.assertTrue(len(results) > 0)
def test_list_wrong_languages(self):
video = videos.Video.from_path(self.path)
with OpenSubtitles(self.config) as service:
results = service.list(video, self.wrong_languages)
self.assertTrue(len(results) == 0)
def test_download(self):
video = videos.Video.from_path(self.path)
with OpenSubtitles(self.config) as service:
subtitle = service.list(video, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
result = service.download(subtitle)
self.assertTrue(isinstance(result, Subtitle))
self.assertTrue(os.path.exists(subtitle.path))
class TheSubDBTestCase(unittest.TestCase):
query_tests = ['test_query', 'test_query_wrong_hash', 'test_query_wrong_languages']
list_tests = ['test_list', 'test_list_wrong_languages']
download_tests = ['test_download']
def setUp(self):
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.path = existing_video
self.hash = u'edc1981d6459c6111fe36205b4aff6c2'
self.wrong_hash = u'ffffffffffffffffffffffffffffffff'
self.languages = set(['en', 'nl'])
self.wrong_languages = set(['zz', 'cs'])
self.fake_file = u'/tmp/fake_file'
def test_query(self):
with TheSubDB(self.config) as service:
results = service.query(self.fake_file, self.hash, self.languages)
self.assertTrue(len(results) > 0)
def test_query_wrong_hash(self):
with TheSubDB(self.config) as service:
results = service.query(self.fake_file, self.wrong_hash, self.languages)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with TheSubDB(self.config) as service:
results = service.query(self.fake_file, self.hash, self.wrong_languages)
self.assertTrue(len(results) == 0)
def test_list(self):
video = videos.Video.from_path(self.path)
with TheSubDB(self.config) as service:
results = service.list(video, self.languages)
self.assertTrue(len(results) > 0)
def test_list_wrong_languages(self):
video = videos.Video.from_path(self.path)
with TheSubDB(self.config) as service:
results = service.list(video, self.wrong_languages)
self.assertTrue(len(results) == 0)
def test_download(self):
video = videos.Video.from_path(self.path)
with TheSubDB(self.config) as service:
subtitle = service.list(video, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
service.download(subtitle)
self.assertTrue(os.path.exists(subtitle.path))
class SubsWikiTestCase(unittest.TestCase):
query_tests = ['test_query_series', 'test_query_movie', 'test_query_wrong_parameters', 'test_query_wrong_series', 'test_query_wrong_languages']
list_tests = ['test_list_series', 'test_list_movie', 'test_list_series_wrong_languages']
download_tests = ['test_download']
def setUp(self):
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.fake_file = u'/tmp/fake_file'
self.languages = set(['en', 'es'])
self.wrong_languages = set(['zz', 'ay'])
self.movie_path = u'Soul Surfer (2011)/Soul.Surfer.(2011).DVDRip.XviD-TWiZTED.mkv'
self.movie_keywords = set(['twizted'])
self.movie = u'Soul Surfer'
self.movie_year = 2011
self.series_path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.series_keywords = set(['asap', 'hdtv'])
self.series = 'The Big Bang Theory'
self.wrong_series = 'No Existent Show Name'
self.series_season = 5
self.series_episode = 6
def test_query_series(self):
with SubsWiki(self.config) as service:
results = service.query(self.fake_file, self.languages, keywords=self.series_keywords, series=self.series, season=self.series_season, episode=self.series_episode)
self.assertTrue(len(results) > 0)
def test_query_movie(self):
with SubsWiki(self.config) as service:
results = service.query(self.fake_file, self.languages, keywords=self.movie_keywords, movie=self.movie, year=self.movie_year)
self.assertTrue(len(results) > 0)
def test_query_wrong_parameters(self):
with SubsWiki(self.config) as service:
with self.assertRaises(ServiceError):
service.query(self.fake_file, self.languages, keywords=self.movie_keywords, movie=self.movie, series=self.series)
def test_query_wrong_series(self):
with SubsWiki(self.config) as service:
results = service.query(self.fake_file, self.languages, keywords=self.series_keywords, series=self.wrong_series, season=self.series_season, episode=self.series_episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with SubsWiki(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, keywords=self.series_keywords, series=self.series, season=self.series_season, episode=self.series_episode)
self.assertTrue(len(results) == 0)
def test_list_series(self):
video = videos.Video.from_path(self.series_path)
with SubsWiki(self.config) as service:
results = service.list(video, self.languages)
self.assertTrue(len(results) > 0)
def test_list_movie(self):
video = videos.Video.from_path(self.movie_path)
with SubsWiki(self.config) as service:
results = service.list(video, self.languages)
self.assertTrue(len(results) > 0)
def test_list_series_wrong_languages(self):
video = videos.Video.from_path(self.series_path)
with SubsWiki(self.config) as service:
results = service.list(video, self.wrong_languages)
self.assertTrue(len(results) == 0)
def test_download(self):
video = videos.Video.from_path(self.series_path)
with SubsWiki(self.config) as service:
subtitle = service.list(video, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
service.download(subtitle)
self.assertTrue(os.path.exists(subtitle.path))
class SubtitulosTestCase(unittest.TestCase):
query_tests = ['test_query', 'test_query_wrong_series', 'test_query_wrong_languages']
list_tests = ['test_list', 'test_list_wrong_languages']
download_tests = ['test_download']
def setUp(self):
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.fake_file = u'/tmp/fake_file'
self.languages = set(['en', 'es'])
self.wrong_languages = set(['zz', 'ay'])
self.path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.keywords = set(['asap', 'hdtv'])
self.series = 'The Big Bang Theory'
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
def test_query(self):
with Subtitulos(self.config) as service:
results = service.query(self.fake_file, self.languages, self.keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with Subtitulos(self.config) as service:
results = service.query(self.fake_file, self.languages, self.keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with Subtitulos(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, self.keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_list(self):
video = videos.Video.from_path(self.path)
with Subtitulos(self.config) as service:
results = service.list(video, self.languages)
self.assertTrue(len(results) > 0)
def test_list_wrong_languages(self):
video = videos.Video.from_path(self.path)
with Subtitulos(self.config) as service:
results = service.list(video, self.wrong_languages)
self.assertTrue(len(results) == 0)
def test_download(self):
video = videos.Video.from_path(self.path)
with Subtitulos(self.config) as service:
subtitle = service.list(video, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
result = service.download(subtitle)
self.assertTrue(os.path.exists(subtitle.path))
def query_suite():
suite = unittest.TestSuite()
suite.addTests(map(BierDopjeTestCase, BierDopjeTestCase.query_tests))
suite.addTests(map(OpenSubtitlesTestCase, OpenSubtitlesTestCase.query_tests))
suite.addTests(map(TheSubDBTestCase, TheSubDBTestCase.query_tests))
suite.addTests(map(SubsWikiTestCase, SubsWikiTestCase.query_tests))
suite.addTests(map(SubtitulosTestCase, SubtitulosTestCase.query_tests))
return suite
def list_suite():
suite = unittest.TestSuite()
suite.addTests(map(BierDopjeTestCase, BierDopjeTestCase.list_tests))
suite.addTests(map(OpenSubtitlesTestCase, OpenSubtitlesTestCase.list_tests))
suite.addTests(map(TheSubDBTestCase, TheSubDBTestCase.list_tests))
suite.addTests(map(SubsWikiTestCase, SubsWikiTestCase.list_tests))
suite.addTests(map(SubtitulosTestCase, SubtitulosTestCase.list_tests))
return suite
def download_suite():
suite = unittest.TestSuite()
suite.addTests(map(BierDopjeTestCase, BierDopjeTestCase.download_tests))
suite.addTests(map(OpenSubtitlesTestCase, OpenSubtitlesTestCase.download_tests))
suite.addTests(map(TheSubDBTestCase, TheSubDBTestCase.download_tests))
suite.addTests(map(SubsWikiTestCase, SubsWikiTestCase.download_tests))
suite.addTests(map(SubtitulosTestCase, SubtitulosTestCase.download_tests))
return suite
if __name__ == '__main__':
suites = []
suites.append(query_suite())
suites.append(list_suite())
suites.append(download_suite())
unittest.TextTestRunner(verbosity=2).run(unittest.TestSuite(suites))