Compare commits

...

517 Commits

Author SHA1 Message Date
Diaoul ea1fe66884 Update documentation 2015-03-04 23:42:26 +01:00
Diaoul 2786f66c2a Update release notes 2015-03-04 23:41:34 +01:00
Diaoul eef70c4ed8 Bump version to 0.7.5 2015-03-04 20:47:37 +01:00
Diaoul 253b8ac69e Add python 2.6 to classifiers and travis 2015-03-04 20:47:08 +01:00
Diaoul e48bf7de02 Add PyCharm to gitignore 2015-03-04 20:46:32 +01:00
Antoine Bertin 8c040f2197 Merge pull request #404 from caronc/0.7.x
0.7.x
2015-03-04 20:31:06 +01:00
Chris Caron 407cbe1679 series/movie title sanitization refactored 2015-02-01 15:46:43 -05:00
Chris Caron 16ddb8a02e addic7ed timeout reverted back to 10 second timeout 2015-02-01 14:48:14 -05:00
Chris Caron c74aa01c09 random user agent determined at runtime; default user-agent reference centralized 2015-02-01 14:46:35 -05:00
Chris Caron 1b7291e45c Merge branch '0.7.x' of github:caronc/subliminal into 0.7.x 2015-01-30 16:38:45 -05:00
Chris Caron a86694d8d1 updated requirements so that Travis CI doesn't haul in guessit v0.10 2015-01-30 16:38:16 -05:00
Chris Caron e45fe70c95 updated requirements 2015-01-28 19:29:51 -05:00
Chris Caron 6c2833612c Merge branch '0.7.x' of github:caronc/subliminal into 0.7.x 2015-01-28 16:48:44 -05:00
Chris Caron 3acbefb9c2 fixes #428; I'm not proud of this fix; but it resolves the issue.
Hopefully the Addic7ed administrator can respond to my email inquiring as to why they are blocking us by the User-Agent string.

Automation is the 21st century of the internet; No one wants to click 8
times past banners just to get a 1KB (in size) subtitle. Most people
have Ad blocking software and don't even see these banners anyway. There
are many other ways to get people on board with helping them out
financially (if that's what this is about), and at the same time
accomodate those who've automated their service. I will roll back this
commit when we can come to a better resolution.
2015-01-28 16:48:26 -05:00
Chris Caron a25640b57f fixes #425; I'm not proud of this fix; but it resolves the issue.
Hopefully the Addic7ed administrator can respond to my email inquiring as to why they are blocking us by the User-Agent string.

Automation is the 21st century of the internet; No one wants to click 8
times past banners just to get a 1KB (in size) subtitle. Most people
have Ad blocking software and don't even see these banners anyway. There
are many other ways to get people on board with helping them out
financially (if that's what this is about), and at the same time
accomodate those who've automated their service. I will roll back this
commit when we can come to a better resolution.
2015-01-25 19:57:26 -05:00
Chris Caron d45e3b0941 improved logging for provider debugging 2015-01-25 19:47:53 -05:00
Chris Caron e6bd7040e3 scan_video() now takes pre-guessed video as optional input. This grants
the flexibility for 3rd party apps that wrap the subliminal framework to
do their own guess management. The default behaviour of this fuction
will remain the same as it always has. This new feature will only kick
in if the video is specified to be used instead.
2014-12-07 17:12:08 -05:00
Chris Caron 7fef3cfc93 scoring adjustments moved into compute_score() and some logging cleanup 2014-11-29 20:16:37 -05:00
Chris Caron bc575ea9b2 comparison functions added to help with 3rd party filtering 2014-11-29 20:15:28 -05:00
Chris Caron 3c7634fef2 eliminated bierdopje entry point to fix testing issue 2014-11-11 14:22:17 -05:00
Chris Caron d635269aa0 removed bierdopje provider since it's not referenced anymore anyway (tests for it were removed in an earlier commit) 2014-11-11 14:07:49 -05:00
Chris Caron 90d06e2072 logging output slighly adjusted (added some clarity and removed some unnecessary entries when not debugging) 2014-11-11 14:06:09 -05:00
Chris Caron 87aefa3a4d Massive overhaul on testing to make it Python v2.6 compatible 2014-11-11 13:17:51 -05:00
Chris Caron 6cdf18961e Fixed TVSubtitles.net matching 2014-11-11 13:16:57 -05:00
Chris Caron 827c75f092 better handling of duplicate download prevention 2014-11-11 13:16:12 -05:00
Chris Caron 1e9588e5b8 Added support for titles that contain quotes 2014-10-18 16:18:42 -04:00
Chris Caron 4702284e87 podnapisi website changes maded in Aug 2014 broke this provider in subliminal. Provider now supports new page layout 2014-09-18 10:03:20 -04:00
Chris Caron 88a2cb9681 added graceful handling of subtitle providers that are simply offline or unavailable. 2014-09-18 10:02:22 -04:00
Chris Caron 3b52a9385f added ability to prioritize multiple matched subtitles; Download Hearing Impaired (HI) first before non-HI or vs versa. This is kind of an extension to the bestscore enhancment already added in a previous commit. 2014-09-18 10:00:21 -04:00
Chris Caron 73a4e5a3af subliminal bugfix to prevent multiple matched subtitles from different providers being downloaded. Just download 1 (the best matched); bugfix 2014-09-18 09:52:48 -04:00
Chris Caron 71d206ad46 allow searching for subtitles by best score; not exclusively hearing impaired (HI) or non-HI 2014-09-18 09:51:39 -04:00
Chris Caron 14c7443635 applied guessit v0.7 support 2014-09-18 09:50:56 -04:00
Chris Caron e6dc714f7a Eliminated Dict Comprehensions (PEP 274) references to allow subliminal to work with python v2.4+ 2014-09-18 09:48:22 -04:00
Chris Caron 3b832a4564 updated guessit and babelfish minimum requirements 2014-09-18 09:47:50 -04:00
Antoine Bertin a6abc268a5 Release 0.7.4 2014-01-27 22:21:49 +01:00
Antoine Bertin 2dea4fef44 Update requirements
Exclude newest releases of guessit and babelfish
2014-01-27 22:20:21 +01:00
Antoine Bertin fe76634d02 Release 0.7.3 2013-11-22 21:05:49 +01:00
Antoine Bertin b499540bed Sync README and documentation 2013-11-22 20:47:56 +01:00
Antoine Bertin 7b4a9c2060 Fix wrong error catched for babelfish 0.4.0 2013-11-21 23:52:06 +01:00
Antoine Bertin a84cc80a88 Ignore IDE error in cli 2013-11-21 23:51:25 +01:00
Antoine Bertin 241cea9729 Fix podnapisi tests 2013-11-21 23:31:43 +01:00
Antoine Bertin 4b83ddc63e Improve assertions in tests 2013-11-21 23:31:02 +01:00
Antoine Bertin 0b431fbb8d Add Podnapisi to the list of providers in documentation 2013-11-21 00:38:01 +01:00
Antoine Bertin 3736d921a1 Update dogpile.cache to 0.5.2 and use a MutexLock in cli 2013-11-21 00:12:53 +01:00
Antoine Bertin 380fb28d2e Update to babelfish 0.4.0 2013-11-20 22:40:06 +01:00
Antoine Bertin 64c0ee4ccf Add setuptools to dev-requirements.txt 2013-11-20 22:38:55 +01:00
Antoine Bertin 5977bf69fb Improve embedded subtitles language detection 2013-11-14 22:08:48 +01:00
CelestianX 179ae6a24e Updated README with proper information
Section relative to the library was invalid. Missing references and
arguments.
2013-11-14 21:55:24 +01:00
Antoine Bertin 16942ec4c7 Switch to 0.7.3 2013-11-14 21:53:36 +01:00
Antoine Bertin 6f5378ea40 Be more permissive in subtitle validation 2013-11-14 21:51:39 +01:00
Antoine Bertin 02ee2039f4 Skip empty language in addic7ed 2013-11-14 21:51:04 +01:00
Antoine Bertin e7f89c1a19 Release 0.7.2 2013-11-10 11:06:46 +01:00
Antoine Bertin be4f9d92eb Remove unused import 2013-11-10 11:03:37 +01:00
Antoine Bertin ca63b97e79 Parse IETF language format in cli 2013-11-10 10:25:00 +01:00
Antoine Bertin c1ed4a0232 Fix exception handling when validating subtitle 2013-11-10 10:24:23 +01:00
Antoine Bertin b826a0bf08 Use debug level for subtitle track detection 2013-11-10 10:23:41 +01:00
Antoine Bertin fd0d87d719 Use info level when skipping providers 2013-11-10 10:23:24 +01:00
Antoine Bertin 094373f3c1 Add podnapisi provider 2013-11-10 10:22:33 +01:00
Antoine Bertin 5dac623c9f Update to babelfish 0.3.0 2013-11-09 20:25:39 +01:00
Antoine Bertin 57d1e772ec Use more list comprehension 2013-11-09 18:09:19 +01:00
Antoine Bertin 983efbfd9b Reduce debug logging in opensubtitles 2013-11-09 18:06:52 +01:00
Antoine Bertin 1f11e293c1 Add missing docstring 2013-11-09 18:06:17 +01:00
Antoine Bertin bfd278ae1c Change Subtitle repr language format 2013-11-09 18:05:48 +01:00
Antoine Bertin dbe1b9d2af Update guessit requirement 2013-11-09 03:04:13 +01:00
Antoine Bertin d71bc4bf09 Set CLI default cache expiration time to 30 days 2013-11-07 00:33:38 +01:00
Antoine Bertin faf2e1dfa4 Add a CACHE_VERSION to force cache reloading on version change 2013-11-07 00:26:06 +01:00
Antoine Bertin 93360aa1bb Fix find_show_id in tvsubtitles for ambiguous series 2013-11-06 21:08:09 +01:00
Antoine Bertin 8df7780ef9 Switch to 0.7.2 2013-11-06 00:42:44 +01:00
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
Antoine Bertin e8dec6c143 Merge branch 'develop' 2012-03-25 18:01:53 +02:00
Antoine Bertin 8b2b19de4b Update and fix NEWS 2012-03-25 17:58:42 +02:00
Antoine Bertin af162578c0 Merge branch 'develop' 2012-03-25 17:47:15 +02:00
Antoine Bertin c6ccfa4417 Bump version 2012-03-25 17:46:56 +02:00
Antoine Bertin 132001a1be Improve error handling of enzyme parsing 2012-03-25 17:46:10 +02:00
Antoine Bertin 830c09dde7 Merge branch 'develop' 2012-03-25 13:12:56 +02:00
Antoine Bertin 57bcde2511 Add missing setup.cfg 2012-03-25 13:12:46 +02:00
Antoine Bertin c1db5b43e5 Merge branch 'develop' 2012-03-25 12:50:27 +02:00
Antoine Bertin 7b7f29720c Update README 2012-03-25 12:10:55 +02:00
Antoine Bertin 13320db360 Clean up documentation 2012-03-25 12:10:46 +02:00
Antoine Bertin dd9d63d74e Remove version of requests in requirements.txt 2012-03-25 12:05:35 +02:00
Antoine Bertin 739ed2be6d Update NEWS 2012-03-25 12:04:36 +02:00
Antoine Bertin 95d9ed685f Use unicode for release of ResultSubtitle 2012-03-25 11:53:25 +02:00
Antoine Bertin 1064d60873 Fix setup sdist including everything. Improve keywords and requirements 2012-03-25 10:39:11 +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
Antoine Bertin a93f74c3ec Do not create download tasks without subtitles 2012-03-10 01:43:13 +01:00
Antoine Bertin a5b9d2dbd6 Add missing documentation stuff 2012-03-10 01:12:51 +01:00
Antoine Bertin 2e6e27991a Use modified flask's sphinx documentation theme 2012-03-10 00:50:02 +01:00
Antoine Bertin b0ae70a88f More documentation 2012-03-10 00:19:22 +01:00
Antoine Bertin af2e8ba7bd Fix wrong __all__ in utils 2012-03-10 00:04:37 +01:00
Antoine Bertin 15ca8dac1d More documentation 2012-03-10 00:04:19 +01:00
Antoine Bertin b6f6d17daa More documentation 2012-03-07 08:37:41 +01:00
Nicolas Wack 4d06316d22 ported services to BeautifulSoup4 2012-03-05 22:02:31 +01:00
Antoine Bertin 9290d7e96f Change list_subtitles so it returns a dict of subtitles by video 2012-03-05 21:46:53 +01:00
Antoine Bertin 10f931ab68 Add some core components to subliminal 2012-03-05 21:45:09 +01:00
Antoine Bertin 75986be154 Add some examples to the documentation 2012-03-03 23:19:09 +01:00
Antoine Bertin 1b53c9377d Remove old stuff in README 2012-03-03 23:18:58 +01:00
Antoine Bertin 7b31c000cd Allow zero depth entries 2012-03-03 22:37:31 +01:00
Antoine Bertin d180dd5f6a Fix subswiki and subtitulos 2012-03-03 22:24:34 +01:00
Antoine Bertin b7d4d67bec Add docstrings to async 2012-03-03 18:57:45 +01:00
Antoine Bertin 8a018ed3a8 Update async unittests 2012-03-03 18:27:42 +01:00
Antoine Bertin fe41ab9c6b Rename attribute _workers to workers in Pool 2012-03-03 18:27:23 +01:00
Antoine Bertin 7cf00b4b58 Add content to NEWS 2012-03-03 15:23:10 +01:00
Antoine Bertin 224b2399bc Fix COPYING 2012-03-03 15:22:51 +01:00
Antoine Bertin bef04df3b3 Fix CLI 2012-03-03 10:56:47 +01:00
Antoine Bertin a956707669 Fix async 2012-03-03 10:56:42 +01:00
Antoine Bertin d812af8535 Add a unittest for async 2012-03-02 11:02:47 +01:00
Antoine Bertin 0edb45d851 Add Pool to subliminal 2012-03-02 11:01:44 +01:00
Antoine Bertin e25903f282 Fix services 2012-03-02 10:43:21 +01:00
Antoine Bertin 591f98957f Fix async 2012-03-02 10:43:07 +01:00
Antoine Bertin 01eb6a7a5a Start working on the CLI 2012-03-02 10:42:51 +01:00
Antoine Bertin 2e9fc52ba2 Update unittests 2012-03-01 22:07:53 +01:00
Antoine Bertin 25c5b0f695 Update User-Agent 2012-03-01 19:30:05 +01:00
Antoine Bertin 88b242fe65 Add __version__ to subliminal 2012-03-01 18:00:26 +01:00
Antoine Bertin 28b7414347 Rename Subliminal to subliminal 2012-03-01 18:00:08 +01:00
Antoine Bertin 71e58336aa Remove plugins 2012-03-01 17:48:20 +01:00
Antoine Bertin b3388cfef9 Update unittests 2012-03-01 17:43:31 +01:00
Antoine Bertin ad1cbc8318 Add subswiki, subtitulos and thesubdb services 2012-03-01 17:43:18 +01:00
Antoine Bertin 6d47022bb5 Use new check_validity method in services 2012-03-01 17:42:51 +01:00
Antoine Bertin 4e96c77d2b Add a check_validity method to ServiceBase 2012-03-01 17:42:04 +01:00
Antoine Bertin b1071e0870 Add pass to some ServiceBase methods 2012-03-01 17:40:37 +01:00
Antoine Bertin e786c456af Default method for Service download 2012-03-01 17:40:05 +01:00
Antoine Bertin 68fb07ee0d Remove unused test files 2012-03-01 17:35:09 +01:00
Antoine Bertin 35bf0b803c Remove unused exception 2012-03-01 13:12:13 +01:00
Antoine Bertin ef1a142c59 Clean imports in core 2012-03-01 13:12:02 +01:00
Antoine Bertin 14ff363ce6 Replace plugin with service 2012-03-01 13:11:47 +01:00
Antoine Bertin 7872bca40d Fix very long lines in videos 2012-03-01 13:09:04 +01:00
Antoine Bertin 4b4272ce85 Improve setup.py stuff 2012-03-01 13:07:34 +01:00
Antoine Bertin 28b99ad374 General refactoring
- Move from md to rst
- Add sphinx docstrings
- Rename plugins to services
- Replace Subliminal class with the async module
- Straightforward to use
- Use relative imports
- Remove mkvmerge stuff
2012-02-29 22:28:54 +01:00
Antoine Bertin b7fb9a7703 Rename test to tests 2012-02-27 19:11:58 +01:00
Antoine Bertin 3eb4913863 Merge branch 'develop' of github.com:Diaoul/subliminal into develop 2012-02-27 16:46:42 +01:00
Antoine Bertin fe46afd571 Update license and minor changes 2012-02-27 15:34:23 +01:00
Antoine Bertin e8ba65ef22 Use MATCHING_CONFIDENCE 2012-01-24 15:25:27 +01:00
Antoine Bertin 4854b2630e Fix CLI 2012-01-11 13:21:04 +01:00
Antoine Bertin e7d3465cdd Update required packages 2012-01-02 07:56:06 +01:00
Antoine Bertin b04975af2e Improve __init__ 2012-01-02 07:56:06 +01:00
Antoine Bertin 947384aa6f Use relative imports 2012-01-02 07:56:06 +01:00
Antoine Bertin 8db65824bb Merge pull request #54 from bonega/develop
Fix for "ValueError: zero length field name in format"
2011-12-20 12:12:19 -08:00
Andreas Liljeqvist ec1c625a1a Fix for "ValueError: zero length field name in format"
Python 2.6 must have positional arguments for format.
2011-12-20 20:21:02 +01:00
Antoine Bertin ded2171375 Update README 2011-12-10 10:47:17 +01:00
Antoine Bertin fdc388a117 Fix __all__ of core.py 2011-12-08 16:02:31 +01:00
Antoine Bertin 2320cfd8e5 Fix a bug in matching_confidence 2011-12-08 15:44:23 +01:00
Antoine Bertin 1be88b8454 Fix variable name conflict 2011-12-08 15:43:35 +01:00
Antoine Bertin e3ea8ee06b Fix variable name conflicts 2011-12-08 15:17:14 +01:00
Antoine Bertin d7c10ae1b8 Code clean-up 2011-12-08 15:13:54 +01:00
Antoine Bertin 03f209b2b0 Use enzyme 2011-12-04 22:36:04 +01:00
Antoine Bertin c0ddac5eed Update license 2011-12-04 22:29:29 +01:00
Antoine Bertin 0e06088b3e Update video extensions 2011-12-02 10:53:19 +01:00
Antoine Bertin 75de6bbb3d Add more video extensions 2011-11-29 14:34:15 +01:00
Antoine Bertin 0e0189d518 Fix language detection for EmbeddedSubtitles 2011-11-24 00:06:09 +01:00
Antoine Bertin c166bfd334 Update unittest 2011-11-21 21:23:59 +01:00
Antoine Bertin 496cb3bc68 Fix mkvmerge 2011-11-21 21:23:34 +01:00
Antoine Bertin d5176b1b48 Use langcode instead of language from kaa.metadata 2011-11-17 22:19:53 +01:00
Antoine Bertin 2560c5f711 Fix fromKaa constructor 2011-11-17 08:58:34 +01:00
Antoine Bertin 062fcae014 Use utf-8 encoded strings for comparison 2011-11-17 08:58:15 +01:00
Antoine Bertin 90e6a4b94a Fix core.py for single srt detection 2011-11-17 08:57:57 +01:00
Antoine Bertin c7bf7dcb45 Fix CLI 2011-11-17 08:57:41 +01:00
Antoine Bertin 069ab7405f Delete api.py for now 2011-11-17 08:57:33 +01:00
Antoine Bertin ed2e8aa510 Fix setup.py 2011-11-16 22:46:46 +01:00
Antoine Bertin 08d7ce7a2b Fix missing import 2011-11-14 18:03:07 +01:00
Antoine Bertin 8dabb8cbbd Fix typo in scan 2011-11-11 21:46:44 +01:00
Antoine Bertin d12226eb7e Change version number from 1.1 to 0.5 2011-11-11 20:32:45 +01:00
Antoine Bertin 6d9c5c34f2 Update unittests 2011-11-11 20:23:04 +01:00
Antoine Bertin 251b1fb446 Multiple enhancements
- Add languages.py with language related stuff
- Add multiple Subtitle classes
- Update scan so it returns [(video, [subtitle])]
- Update mkvmerge to support embedded subtitles
- Replace pt-br with (Brazilian, po, pob)
2011-11-11 20:21:06 +01:00
Antoine Bertin f39df2ff46 Update Video and Subtitle. New scan method with kaa.metadata 2011-11-08 01:35:07 +01:00
Antoine Bertin 945bcee3f5 Do not print Popen result in console 2011-11-08 01:33:04 +01:00
Antoine Bertin fef5c09a76 Add mapping for ISO-639-2 languages 2011-11-08 01:31:53 +01:00
Antoine Bertin 54a9851dee Fix mkvmerge 2011-11-06 22:17:49 +01:00
Antoine Bertin b609659f65 Add more logging when skipping videos 2011-11-06 13:55:55 +01:00
Antoine Bertin a352ecc771 Init requests session in PluginBase 2011-11-06 11:27:04 +01:00
Antoine Bertin abbf3bc20f Add a compatibility option to CLI 2011-11-05 23:19:30 +01:00
Antoine Bertin 21704696da Remove unused stuff 2011-11-05 21:42:39 +01:00
Antoine Bertin ffa38694df Add pt-br in valid languages 2011-11-05 21:42:23 +01:00
Antoine Bertin 090bdbeccd Fix "long int exceeds XML-RPC limits" in OpenSubtitles 2011-11-05 21:31:21 +01:00
Antoine Bertin 7f0d9fea54 Update unittests 2011-11-05 21:07:25 +01:00
Antoine Bertin 8f6839dfe8 Add keywords in Subtitulos subtitles 2011-11-05 21:04:27 +01:00
Antoine Bertin 246d35fdb1 Fix Subtitulos list 2011-11-05 21:04:11 +01:00
Antoine Bertin 11b0037986 Fix Subtitulos require_video 2011-11-05 21:03:48 +01:00
Antoine Bertin ab34a23bb4 Fix SubsWiki download 2011-11-05 21:03:34 +01:00
Antoine Bertin 288b9f00e8 Add parameters check in SubsWiki 2011-11-05 21:03:15 +01:00
Antoine Bertin 712842a235 Fix SubsWiki 2011-11-05 21:02:49 +01:00
Antoine Bertin 82b6b3faf5 Fix SubsWiki require_video 2011-11-05 21:02:22 +01:00
Antoine Bertin 563d0f7566 Add parameters check in BierDopje 2011-11-05 21:02:10 +01:00
Antoine Bertin a9c605f7c9 Update OpenSubtitles 2011-11-05 21:01:52 +01:00
Antoine Bertin 21a1c1243b Add Subtitulos to the list 2011-11-05 20:59:07 +01:00
Antoine Bertin 7c94f1792b Use requests in downloadFile 2011-11-05 17:37:00 +01:00
Antoine Bertin b0f87f4e37 Fix scan 2011-11-05 17:36:40 +01:00
Antoine Bertin 4bb10cf89a Fix OpenSubtitles terminate when could not LogIn 2011-11-03 08:50:06 +01:00
Antoine Bertin da22709623 Fix downloadSubtitles 2011-11-03 08:49:29 +01:00
Antoine Bertin 7c8e0457ce Update unittests 2011-11-02 08:57:17 +01:00
Antoine Bertin f18656738d Do not accept Movie in Subtitulos 2011-11-02 08:56:44 +01:00
Antoine Bertin 517cf6205c Add Subtitulos 2011-11-02 08:56:15 +01:00
Antoine Bertin f45f0b492d Fix SubsWiki list 2011-11-02 08:56:04 +01:00
Antoine Bertin 2271d3d4c5 Update unittests 2011-11-02 08:26:37 +01:00
Antoine Bertin b5293e4455 Add SubsWiki 2011-11-02 08:26:12 +01:00
Antoine Bertin c6614e2faa Fix split_keyword 2011-11-02 08:25:48 +01:00
Antoine Bertin b8ddc72fe8 Add other plugins to Subliminal 2011-11-02 08:25:19 +01:00
Antoine Bertin 3e63414b80 Various little changes 2011-11-02 08:25:01 +01:00
Antoine Bertin 58338fc946 Use pt-br in OpenSubtitles 2011-11-02 08:24:19 +01:00
Antoine Bertin acba7d1228 Update unittests for TheSubDB 2011-10-31 22:12:05 +01:00
Antoine Bertin 956d8ef0d8 Update unittests 2011-10-31 22:11:07 +01:00
Antoine Bertin 7bd6db0c5d Add TheSubDB 2011-10-31 22:09:56 +01:00
Antoine Bertin a291af863c Fix GetSubtitle 2011-10-31 22:09:43 +01:00
Antoine Bertin 7f828783a7 Improve HTTP error handling 2011-10-31 22:09:22 +01:00
Antoine Bertin eb57a83bf5 More accurate logging 2011-10-31 22:08:34 +01:00
Antoine Bertin 9720679461 More logging 2011-10-27 01:13:52 +02:00
Antoine Bertin cc10d69b47 Add structure for subtitles.com.br provider 2011-10-27 01:13:35 +02:00
Antoine Bertin e5b8b2fc8e Update unittests 2011-10-27 01:12:29 +02:00
Antoine Bertin b3880b3ec8 Update unittests 2011-10-26 19:28:27 +02:00
Antoine Bertin eaafa7fb25 Fix BierDopje query 2011-10-26 19:27:02 +02:00
Antoine Bertin be346af2ba Fix OpenSubtitles terminate 2011-10-26 19:26:45 +02:00
Antoine Bertin a5d53eaeba Add BierDopje plugin 2011-10-26 08:09:27 +02:00
Antoine Bertin af340eec7b Update CLI 2011-10-25 08:43:01 +02:00
Antoine Bertin 0baac5d829 New setup stuff
- Add requests to make HTTP requests
- Add suds to make SOAP requests
2011-10-25 08:42:43 +02:00
Antoine Bertin 1631d48490 Docstring in subtitles 2011-10-25 08:41:38 +02:00
Antoine Bertin f9874c3c37 Use only one plugin instance within the same worker. Other stuff
- More docstrings
- stopWorkers is more flexible
- pauseWorkers is now just a wrapper
- auto arg for list and download default to False
because of the new with statement
2011-10-25 08:41:16 +02:00
Antoine Bertin 2f8e49b8df Fix bad import. New stuff in __init__ 2011-10-25 08:38:11 +02:00
Antoine Bertin 8455b2c480 New plugins organization
- One file only to avoid importing parent
- Remove shared data and use single plugin instance instead
2011-10-25 08:37:40 +02:00
Antoine Bertin fa25502803 Remove broken plugins from the list 2011-10-25 08:09:08 +02:00
Antoine Bertin bfaa45252a Add plugins and languages args to constructor 2011-10-25 08:07:42 +02:00
Antoine Bertin 9af3627b62 Update OpenSubtitles 2011-10-24 08:46:06 +02:00
Antoine Bertin 8edccedbbd Fix OpenSubtitles when no data is returned 2011-10-24 08:45:49 +02:00
Antoine Bertin 8d020fd39a Update abstract methods in PluginBase 2011-10-24 08:45:23 +02:00
Antoine Bertin 139314eaba Remove useless stuff in PluginBase 2011-10-24 08:44:56 +02:00
Antoine Bertin 9cbd5ccf05 Improve __init__ in PluginBase 2011-10-24 08:42:48 +02:00
Antoine Bertin 314cab60aa Fix wrong state in pauseWorkers 2011-10-24 08:42:07 +02:00
Antoine Bertin 025b53092e Improve shared data 2011-10-24 08:41:48 +02:00
Antoine Bertin 7a0ea2bdff Implement with statement in Subliminal 2011-10-24 08:41:26 +02:00
Antoine Bertin 0a79820dd6 Improve __init__ 2011-10-24 08:39:35 +02:00
Antoine Bertin 7203ad93e4 Fix error in logging when video has no path 2011-10-24 08:39:09 +02:00
Antoine Bertin 504dda817a Put workers in property to raise BadStateError 2011-10-24 08:38:24 +02:00
Antoine Bertin 1069cff6cf Fix DownloadTask put in the taskQueue 2011-10-24 08:36:35 +02:00
Antoine Bertin 2662150710 Fix CLI 2011-10-24 08:34:58 +02:00
Antoine Bertin ad7c187201 Update OpenSubtitles 2011-10-22 18:26:30 +02:00
Antoine Bertin d7125413e5 Fix get_subtitle_path 2011-10-22 18:26:01 +02:00
Antoine Bertin 87d2d9e1f2 Init keywords with empty set in Subtitle 2011-10-22 17:57:49 +02:00
Antoine Bertin 9d712b86fa Improve the use of shared data
- Plugins have now access to their shared data only
- Add a class attribute shared_support in plugins
2011-10-22 17:57:17 +02:00
Antoine Bertin 69baf1e039 Add a video attribute to DownloadTask 2011-10-22 17:49:34 +02:00
Antoine Bertin 906707f0e0 Add size attribute and init hashes in constructor for Video 2011-10-22 17:48:28 +02:00
Antoine Bertin 3762ab1878 Review the factory method for videos 2011-10-22 17:46:14 +02:00
Antoine Bertin 12d7fe7ebf Fix a bug in scan 2011-10-22 17:45:26 +02:00
Antoine Bertin 5a6c278167 Finalize new structure
- Rename files_mode to filemode
- New PluginConfig object to give configuration to plugins
- Improve subtitles ordering with matching_confidence method
- Share plugins data inside a same worker to improve performances
- Fix get_subtitle_path
- Remove KEYWORD_SEPARATORS
- Replace keywords by guess in videos
- New get_keywords(guess) method
2011-10-22 15:21:00 +02:00
Antoine Bertin bddfa15eb8 Working OpenSubtitles 2011-10-20 08:50:21 +02:00
Antoine Bertin 899de2ebe3 More work on 1.1
- Introduce the sort_order so users can specify in which order
subtitles are downloaded (prefer language over plugin, etc.)
- Use key function in sorting
- List method now returns a list of tuples: [(video, [subtitle])]
- OpenSubtitles refactoring
- get_subtitle_path method in subtitles
- imdbid in Video
2011-10-19 00:19:42 +02:00
Antoine Bertin eef578b939 More work on 1.1
- Lots of #TODO
- Differentiate missing language and invalid language exceptions
- Extensions for videos and subtitles
- Mimetypes in videos
- Review PluginBase: use more classmethods
- Add mkvmerge method in videos
- Update unittests
- New utils.py for commonly used functions
2011-10-17 00:55:00 +02:00
Antoine Bertin a7608171b1 Update .gitignore 2011-10-17 00:49:19 +02:00
Antoine Bertin 4d3b050a81 Changes for 1.1
- Review code organization
- Add Video classes
- Improve Subtitle class
- Use classmethods
- Remove all socket.setdefaulttimeout
- Replace xmlrpclib.Server (deprecated) by xmlrpclib.ServerProxy
2011-10-08 18:52:36 +02:00
65 changed files with 4029 additions and 2137 deletions
+5
View File
@@ -0,0 +1,5 @@
[report]
exclude_lines =
def __repr__
raise NotImplementedError
if __name__ == .__main__.:
+43 -5
View File
@@ -1,8 +1,46 @@
build
*.py[co]
# Packages
*.egg
*.egg-info
dist
subliminal.egg-info
*.pyc
.settings
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
# Pydev
.project
.pydevproject
*~
.settings
# Rope
.ropeproject
# PyCharm
.idea
# Sphinx
docs/_build
# Subliminal unittests
subliminal/tests/*.srt
subliminal/tests/*_files
subliminal/tests/*_cache
+3
View File
@@ -0,0 +1,3 @@
[submodule "docs/_themes"]
path = docs/_themes
url = git://github.com/Diaoul/diaoul-sphinx-themes.git
+25
View File
@@ -0,0 +1,25 @@
language: python
python:
- "2.6"
- "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
+167
View File
@@ -0,0 +1,167 @@
Changelog
=========
0.7.5
-----
**release date:** 2015-03-04
* Update requirements
* Remove BierDopje provider
* Add pre-guessed video optional argument in scan_video
* Improve hearing impaired support
* Fix TVSubtitles and Podnapisi providers
0.7.4
-----
**release date:** 2014-01-27
* Fix requirements for guessit and babelfish
0.7.3
-----
**release date:** 2013-11-22
* Fix windows compatibility
* Improve subtitle validation
* Improve embedded subtitle languages detection
* Improve unittests
0.7.2
-----
**release date:** 2013-11-10
* Fix TVSubtitles for ambiguous series
* Add a CACHE_VERSION to force cache reloading on version change
* Set CLI default cache expiration time to 30 days
* Add podnapisi provider
* Support script for languages e.g. Latn, Cyrl
* Improve logging levels
* Fix subtitle validation in some rare cases
0.7.1
-----
**release date:** 2013-11-06
* Improve CLI
* Add login support for Addic7ed
* Remove lxml dependency
* Many fixes
0.7.0
-----
**release date:** 2013-10-29
**WARNING:** Complete rewrite of subliminal with backward incompatible changes
* Use enzyme to parse metadata of videos
* Use babelfish to handle languages
* Use dogpile.cache for caching
* Use charade to detect subtitle encoding
* Use pysrt for subtitle validation
* Use entry points for subtitle providers
* New subtitle score computation
* Hearing impaired subtitles support
* Drop async support
* Drop a few providers
* And much more...
0.6.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
View File
@@ -0,0 +1 @@
include LICENSE HISTORY.rst requirements.txt
-6
View File
@@ -1,6 +0,0 @@
subliminal
==========
Python module to search and download subtitles
* [Project page](https://github.com/Diaoul/subliminal)
* [Initial project by Patrick Dessalle](http://code.google.com/p/periscope/)
+56
View File
@@ -0,0 +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.
.. image:: https://travis-ci.org/Diaoul/subliminal.png?branch=develop
:target: https://travis-ci.org/Diaoul/subliminal
.. 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
* OpenSubtitles
* Podnapisi
* TheSubDB
* TvSubtitles
Usage
-----
CLI
^^^
Download english subtitles::
$ subliminal -l en -- The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
1 subtitle downloaded
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::
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'})
# scan for videos in the folder and their subtitles
videos = subliminal.scan_videos(['/path/to/video/folder'], subtitles=True, embedded_subtitles=True, age=timedelta(weeks=1))
# download
subliminal.download_best_subtitles(videos, {Language('eng'), Language('fra')}, age=timedelta(week=1))
License
-------
MIT
+5
View File
@@ -0,0 +1,5 @@
sympy>=0.7.3
sphinx>=1.1.3
sphinxcontrib-programoutput>=0.8
Sphinx-PyPI-upload>=0.2.1
setuptools>=1.4
+177
View File
@@ -0,0 +1,177 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
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
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@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)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/subliminal.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/subliminal.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/subliminal"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/subliminal"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(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
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
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."
+4
View File
@@ -0,0 +1,4 @@
<h3>Subliminal</h3>
<p>
Subliminal is a Python library to search and download subtitles.
</p>
Submodule
+1
Submodule docs/_themes added at 24aa9748e4
+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
+11
View File
@@ -0,0 +1,11 @@
Cache
=====
.. module:: subliminal.cache
.. autodata:: CACHE_VERSION
.. autofunction:: subliminal_key_generator
.. data:: region
The dogpile.cache region
Refer to `dogpile.cache's documentation <http://dogpilecache.readthedocs.org>`_ to see how to configure the 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
+269
View File
@@ -0,0 +1,269 @@
# -*- coding: utf-8 -*-
#
# subliminal documentation build configuration file, created by
# sphinx-quickstart on Wed Oct 23 23:24:28 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# 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
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# 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', 'sphinxcontrib.programoutput']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
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.__version__
# The full version, including alpha/beta/rc tags.
release = subliminal.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
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 = '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 = {'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']
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#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
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
html_sidebars = {
'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.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'subliminaldoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'subliminal.tex', u'subliminal Documentation',
u'Antoine Bertin', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'subliminal', u'subliminal Documentation',
[u'Antoine Bertin'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'subliminal', u'subliminal Documentation',
u'Antoine Bertin', 'subliminal', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# 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'
+102
View File
@@ -0,0 +1,102 @@
.. subliminal documentation master file, created by
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.
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.
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
* OpenSubtitles
* Podnapisi
* TheSubDB
* TvSubtitles
Usage
-----
CLI
^^^
Download english subtitles::
$ subliminal -l en -- The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
1 subtitle downloaded
See :mod:`subliminal.cli`
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::
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'})
# scan for videos in the folder and their subtitles
videos = subliminal.scan_videos(['/path/to/video/folder'], subtitles=True, embedded_subtitles=True, age=timedelta(weeks=1))
# 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
provider_guide
API Documentation
-----------------
If you are looking for information on a specific function, class or method,
this part of the documentation is for you.
.. 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.
+9
View File
@@ -0,0 +1,9 @@
beautifulsoup4>=4.3.2
guessit>=0.7,<0.10
requests>=2.0.1
enzyme>=0.4.0
html5lib>=0.99
dogpile.cache>=0.5.2
babelfish>=0.5.0
charade>=1.0.3
pysrt>=0.5.0
-80
View File
@@ -1,80 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import subliminal
import logging
import mimetypes
import os
import os.path
import sys
def main():
'''Download subtitles'''
# parse command line options
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('-p', '--plugin', action='append', dest='plugins', help='plugin 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)
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 plugins may not work)')
parser.add_argument('--version', action='version', version=subliminal.__version__)
parser.add_argument('paths', nargs='+', help='path to video files or folders', 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')
# Initialize the instance
subli = subliminal.Subliminal(cache_dir=args.cache_dir, workers=args.workers, multi=args.multi, force=args.force, max_depth=3, files_mode=-1)
if args.plugins:
subli.plugins = args.plugins
if args.languages:
subli.languages = args.languages
try:
subtitles = subli.downloadSubtitles([unicode(x) for x in args.paths])
finally:
subli.stopWorkers()
if len(subtitles) == 0:
if not args.quiet:
sys.stderr.write('No subtitles found\n')
exit(1)
if not args.quiet:
print '*' * 50
print 'Downloaded %s subtitles' % len(subtitles)
for subtitle in subtitles:
print subtitle
print '*' * 50
if __name__ == '__main__':
main()
+7
View File
@@ -0,0 +1,7 @@
[build_sphinx]
source-dir = docs/
build-dir = docs/_build
all_files = 1
[upload_sphinx]
upload-dir = docs/_build/html
+33 -38
View File
@@ -1,45 +1,40 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from setuptools import setup
from setuptools import setup, find_packages
execfile('subliminal/version.py')
setup(name='subliminal',
version=__version__,
license='LGPLv3',
description='Subliminal - Subtitles, faster than your thoughts',
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',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Multimedia :: Video'],
keywords='subliminal video movie subtitle python library',
version='0.7.5',
license='MIT',
description='Subtitles, faster than your thoughts',
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=['subliminal', 'subliminal/plugins'],
scripts=['scripts/subliminal'],
py_modules=['subliminal'],
install_requires=['BeautifulSoup>=3.2.0', 'guessit>=0.2'])
packages=find_packages(),
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.6',
'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',
'opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider',
'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider',
'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider',
'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider'],
'babelfish.language_converters': ['addic7ed = subliminal.converters.addic7ed:Addic7edConverter',
'podnapisi = subliminal.converters.podnapisi:PodnapisiConverter',
'tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter']
},
install_requires=open('requirements.txt').readlines(),
test_suite='subliminal.tests.suite')
+17 -23
View File
@@ -1,25 +1,19 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__title__ = 'subliminal'
__version__ = '0.7.5'
__author__ = 'Antoine Bertin'
__license__ = 'MIT'
__copyright__ = 'Copyright 2013 Antoine Bertin'
__all__ = ['FORMATS', 'LANGUAGES', 'PLUGINS', 'API_PLUGINS', 'Subliminal', 'Subtitle']
from classes import *
from subliminal import *
from version import __version__
import logging
from .api import PROVIDERS_ENTRY_POINT, list_subtitles, download_subtitles, download_best_subtitles
from .cache import MutexLock, 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
class NullHandler(logging.Handler):
def emit(self, record):
pass
logging.getLogger(__name__).addHandler(NullHandler())
+334
View File
@@ -0,0 +1,334 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import collections
import io
import logging
import operator
import babelfish
import pkg_resources
from os.path import basename
from .exceptions import ProviderNotAvailable, InvalidSubtitle
from .subtitle import get_subtitle_path
from socket import error as socket_error
logger = logging.getLogger(__name__)
#: Entry point for the providers
PROVIDERS_ENTRY_POINT = 'subliminal.providers'
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.video.Video` => [:class:`~subliminal.subtitle.Subtitle`]
"""
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.info('Skipping provider %r: no language to search for', provider_entry_point.name)
continue
provider_videos = [v for v in videos if Provider.check(v)]
if not provider_videos:
logger.info('Skipping provider %r: no video to search for', provider_entry_point.name)
continue
# list subtitles with the provider
try:
with Provider(**provider_configs.get(provider_entry_point.name, {})) as provider:
for provider_video in provider_videos:
provider_video_languages = provider_languages - provider_video.subtitle_languages
if not provider_video_languages:
logger.debug('Skipping provider %r: no language to search for for video %r',
provider_entry_point.name, provider_video)
continue
logger.info('Listing subtitles with provider %r for video %r with languages %r',
provider_entry_point.name, provider_video, provider_video_languages)
try:
provider_subtitles = provider.list_subtitles(provider_video, provider_video_languages)
except ProviderNotAvailable as err:
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
logger.debug('ProviderNotAvailable error: %r', str(err))
break
except:
logger.exception('Unexpected error in provider %r', provider_entry_point.name)
continue
logger.info('Found %d subtitle(s) on %s' % (
len(provider_subtitles),
provider_entry_point.name,
))
subtitles[provider_video].extend(provider_subtitles)
except ProviderNotAvailable as err:
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
logger.debug('ProviderNotAvailable error: %r', str(err))
return subtitles
def download_subtitles(subtitles, provider_configs=None, single=False):
"""Download subtitles
: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
"""
provider_configs = provider_configs or {}
discarded_providers = set()
providers_by_name = dict([(ep.name, ep.load()) for ep in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT)])
initialized_providers = {}
downloaded_subtitles = collections.defaultdict(list)
fetched_subtitles = set()
try:
for video, video_subtitles in subtitles.items():
languages = set([subtitle.language for subtitle in video_subtitles])
downloaded_languages = set()
for subtitle in video_subtitles:
# filter
if subtitle.language in downloaded_languages:
logger.debug('Skipping subtitle: %r already downloaded', subtitle.language)
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 as err:
logger.warning('Provider %r is not available, discarding it', subtitle.provider_name)
logger.debug('ProviderNotAvailable error: %r', str(err))
discarded_providers.add(subtitle.provider_name)
continue
except socket_error as err:
logger.warning('Provider %r is not responding, discarding it', subtitle.provider_name)
logger.debug('Provider socket error: %r', str(err))
discarded_providers.add(subtitle.provider_name)
continue
except:
logger.exception('Unexpected error in provider %r', 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)
if basename(subtitle_path) in fetched_subtitles:
logger.debug('Skipping subtitle already retrieved %r', basename(subtitle_path))
continue
logger.info('Downloading subtitle %r into %r', subtitle, subtitle_path)
try:
subtitle_text = provider.download_subtitle(subtitle)
downloaded_subtitles[video].append(subtitle)
except ProviderNotAvailable as err:
logger.warning('Provider %r is not available, discarding it', subtitle.provider_name)
logger.debug('ProviderNotAvailable error: %r', str(err))
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)
fetched_subtitles.add(basename(subtitle_path))
if single or sorted(downloaded_languages) == sorted(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)
return downloaded_subtitles
def download_best_subtitles(videos, languages, providers=None, provider_configs=None, single=False, min_score=0,
hearing_impaired=False, hi_score_adjust=0):
"""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
:param int hi_score_adjust: Adjust hearing_impaired_scores if matched.
"""
provider_configs = provider_configs or {}
discarded_providers = set()
downloaded_subtitles = collections.defaultdict(list)
fetched_subtitles = set()
# 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: video type not hosted here.', provider_entry_point.name)
continue
provider = Provider(**provider_configs.get(provider_entry_point.name, {}))
try:
provider.initialize()
except ProviderNotAvailable as err:
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
logger.debug('ProviderNotAvailable error: %r', str(err))
continue
except socket_error as err:
logger.warning('Provider %r is not responding, discarding it', provider_entry_point.name)
logger.debug('Provider socket error: %r', str(err))
continue
except:
logger.exception('Unexpected error in provider %r', provider_entry_point.name)
continue
initialized_providers[provider_entry_point.name] = provider
try:
for video in videos:
# search for subtitles
subtitles = []
downloaded_languages = set()
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 as err:
logger.warning('Provider %r is not available, discarding it', provider_name)
logger.debug('ProviderNotAvailable error: %r', str(err))
discarded_providers.add(provider_name)
continue
except:
logger.exception('Unexpected error in provider %r', provider_name)
continue
logger.info('Found %d subtitle(s) on %s' % (
len(provider_subtitles),
provider_name,
))
subtitles.extend(provider_subtitles)
# find the best subtitles and download them
for subtitle, score in sorted([(s, s.compute_score(video, hi_score_adjust)) \
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 hearing_impaired is not None:
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)
if basename(subtitle_path) in fetched_subtitles:
logger.debug('Skipping subtitle already retrieved %r', basename(subtitle_path))
continue
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 as err:
logger.warning('Provider %r is not available, discarding it', subtitle.provider_name)
logger.debug('ProviderNotAvailable error: %r', str(err))
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)
fetched_subtitles.add(basename(subtitle_path))
if single or sorted(downloaded_languages) == sorted(languages):
break
finally: # terminate providers
for (provider_name, provider) in initialized_providers.items():
try:
provider.terminate()
except ProviderNotAvailable as err:
logger.warning('Provider %r is not available, unable to terminate', provider_name)
logger.debug('ProviderNotAvailable error: %r', str(err))
except socket_error as err:
logger.warning('Provider %r is not available, unable to terminate', provider_name)
logger.debug('Provider socket error: %r', str(err))
except:
logger.exception('Unexpected error in provider %r', provider_name)
return downloaded_subtitles
+56
View File
@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
import inspect
from dogpile.cache import make_region # @UnresolvedImport
from dogpile.cache.backends.file import AbstractFileLock # @UnresolvedImport
from dogpile.cache.compat import string_type # @UnresolvedImport
from dogpile.core.readwrite_lock import ReadWriteMutex # @UnresolvedImport
#: Subliminal's cache version
CACHE_VERSION = 2
def subliminal_key_generator(namespace, fn, to_str=string_type):
"""Add a :data:`CACHE_VERSION` to dogpile.cache's default function_key_generator"""
if namespace is None:
namespace = '%d:%s:%s' % (CACHE_VERSION, fn.__module__, fn.__name__)
else:
namespace = '%d:%s:%s|%s' % (CACHE_VERSION, fn.__module__, fn.__name__, namespace)
args = inspect.getargspec(fn)
has_self = args[0] and args[0][0] in ('self', 'cls')
def generate_key(*args, **kw):
if kw:
raise ValueError('Keyword arguments not supported')
if has_self:
args = args[1:]
return namespace + '|' + ' '.join(map(to_str, args))
return generate_key
class MutexLock(AbstractFileLock):
""":class:`MutexLock` is a thread-based rw lock based on :class:`dogpile.core.ReadWriteMutex`"""
def __init__(self, filename):
self.mutex = ReadWriteMutex()
def acquire_read_lock(self, wait):
ret = self.mutex.acquire_read_lock(wait)
return wait or ret
def acquire_write_lock(self, wait):
ret = self.mutex.acquire_write_lock(wait)
return wait or ret
def release_read_lock(self):
return self.mutex.release_read_lock()
def release_write_lock(self):
return self.mutex.release_write_lock()
#: The dogpile.cache region (long-lived)
region = make_region(function_key_generator=subliminal_key_generator)
#: The dogpile.cache region for :meth:`~subliminal.providers.Provider.query` (short-lived)
query_region = make_region(function_key_generator=subliminal_key_generator)
-127
View File
@@ -1,127 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
class Error(Exception):
"""Base class for exceptions in subliminal"""
pass
class BadStateError(Error):
"""Exception raised when an invalid action is asked
Attributes:
current -- current state of Subliminal instance
expected -- expected state of Subliminal instance
"""
def __init__(self, current, expected):
self.current = current
self.expected = expected
def __str__(self):
return 'Expected state %d but current state is %d' % (self.expected, self.current)
class LanguageError(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 PluginError(Error):
""""Exception raised when invalid plugin is submitted
Attributes:
plugin -- plugin that cause the error
"""
def __init__(self, plugin):
self.plugin = plugin
def __str__(self):
return self.plugin
class WrongTaskError(Error):
""""Exception raised when invalid task is submitted"""
pass
class DownloadFailedError(Error):
""""Exception raised when a download task has failed in plugin"""
pass
class Subtitle(object):
"""Subtitle class
Attributes:
video_path -- path to the video file
path -- path to the subtitle file
plugin -- plugin used
language -- language of the subtitle
link -- download link
release -- release group identified by guessit
teams -- identified by subliminal
"""
def __init__(self, video_path=None, path=None, plugin=None, language=None, link=None, release=None, teams=None):
self.video_path = video_path
self.path = path
self.plugin = plugin
self.language = language
self.link = link
self.release = release
self.teams = teams
def __repr__(self):
return repr({'video_path': self.video_path, 'path': self.path, 'plugin': self.plugin,
'language': self.language, 'link': self.link, 'release': self.release, 'teams': self.teams})
class Task(object):
"""Base class for tasks to use in subliminal"""
pass
class ListTask(Task):
"""List task to list subtitles"""
def __init__(self, filepath, languages, plugin, config):
self.filepath = filepath
self.plugin = plugin
self.languages = languages
self.config = config
class DownloadTask(Task):
"""Download task to download subtitles"""
def __init__(self, subtitles):
self.subtitles = subtitles
class StopTask(Task):
"""Stop task to stop workers"""
pass
+157
View File
@@ -0,0 +1,157 @@
# -*- 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, MutexLock, 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 IETF codes e.g. fr, pt-BR, sr-Cyrl ')
# configuration
configuration_group = parser.add_argument_group('configuration')
configuration_group.add_argument('-s', '--single', action='store_true',
help='download without language code in subtitle\'s filename i.e. .srt only')
configuration_group.add_argument('-c', '--cache-file', default=DEFAULT_CACHE_FILE,
help='cache file (default: %(default)s)')
# filtering
filtering_group = parser.add_argument_group('filtering')
providers = [ep.name for ep in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT)]
filtering_group.add_argument('-p', '--providers', nargs='+', metavar='PROVIDER',
help='providers to use (%s)' % ', '.join(providers))
filtering_group.add_argument('-m', '--min-score', type=int,
help='minimum score for subtitles (0-%d for episodes, 0-%d for movies)'
% (Episode.scores['hash'], Movie.scores['hash']))
filtering_group.add_argument('-a', '--age', help='download subtitles for videos newer than AGE e.g. 12h, 1w2d')
filtering_group.add_argument('-h', '--hearing-impaired', action='store_true',
help='download hearing impaired subtitles')
filtering_group.add_argument('-f', '--force', action='store_true',
help='force subtitle download for videos with existing subtitles')
# 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 = set( babelfish.Language.fromietf(l) for l in args.languages )
except babelfish.Error:
parser.error('argument -l/--languages: codes are not IETF: %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(**dict([(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 = {}
# 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', expiration_time=datetime.timedelta(days=30), # @UndefinedVariable
arguments={'filename': args.cache_file, 'lock_factory': MutexLock})
# 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, info=['filename'])) 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
+31
View File
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from babelfish import LanguageReverseConverter, language_converters
class Addic7edConverter(LanguageReverseConverter):
def __init__(self):
self.name_converter = language_converters['name']
self.from_addic7ed = {'Català': ('cat',), 'Chinese (Simplified)': ('zho',), 'Chinese (Traditional)': ('zho',),
'Euskera': ('eus',), 'Galego': ('glg',), 'Greek': ('ell',), 'Malay': ('msa',),
'Portuguese (Brazilian)': ('por', 'BR'), 'Serbian (Cyrillic)': ('srp', None, 'Cyrl'),
'Serbian (Latin)': ('srp',), 'Spanish (Latin America)': ('spa',),
'Spanish (Spain)': ('spa',)}
self.to_addic7ed = {('cat',): 'Català', ('zho',): 'Chinese (Simplified)', ('eus',): 'Euskera',
('glg',): 'Galego', ('ell',): 'Greek', ('msa',): 'Malay',
('por', 'BR'): 'Portuguese (Brazilian)', ('srp', None, 'Cyrl'): 'Serbian (Cyrillic)'}
self.codes = self.name_converter.codes | set(self.from_addic7ed.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3, country, script) in self.to_addic7ed:
return self.to_addic7ed[(alpha3, country, script)]
if (alpha3, country) in self.to_addic7ed:
return self.to_addic7ed[(alpha3, country)]
if (alpha3,) in self.to_addic7ed:
return self.to_addic7ed[(alpha3,)]
return self.name_converter.convert(alpha3, country, script)
def reverse(self, addic7ed):
if addic7ed in self.from_addic7ed:
return self.from_addic7ed[addic7ed]
return self.name_converter.reverse(addic7ed)
+32
View File
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from babelfish import LanguageReverseConverter, LanguageConvertError, LanguageReverseError
class PodnapisiConverter(LanguageReverseConverter):
def __init__(self):
self.from_podnapisi = {2: ('eng',), 28: ('spa',), 26: ('pol',), 36: ('srp',), 1: ('slv',), 38: ('hrv',),
9: ('ita',), 8: ('fra',), 48: ('por', 'BR'), 23: ('nld',), 12: ('ara',), 13: ('ron',),
33: ('bul',), 32: ('por',), 16: ('ell',), 15: ('hun',), 31: ('fin',), 30: ('tur',),
7: ('ces',), 25: ('swe',), 27: ('rus',), 24: ('dan',), 22: ('heb',), 51: ('vie',),
52: ('fas',), 5: ('deu',), 14: ('spa', 'AR'), 54: ('ind',), 47: ('srp', None, 'Cyrl'),
3: ('nor',), 20: ('est',), 10: ('bos',), 17: ('zho',), 37: ('slk',), 35: ('mkd',),
11: ('jpn',), 4: ('kor',), 29: ('sqi',), 6: ('isl',), 19: ('lit',), 46: ('ukr',),
44: ('tha',), 53: ('cat',), 56: ('sin',), 21: ('lav',), 40: ('cmn',), 55: ('msa',),
42: ('hin',), 50: ('bel',)}
self.to_podnapisi = dict([(v, k) for k, v in self.from_podnapisi.items()])
self.codes = set(self.from_podnapisi.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3,) in self.to_podnapisi:
return self.to_podnapisi[(alpha3,)]
if (alpha3, country) in self.to_podnapisi:
return self.to_podnapisi[(alpha3, country)]
if (alpha3, country, script) in self.to_podnapisi:
return self.to_podnapisi[(alpha3, country, script)]
raise LanguageConvertError(alpha3, country, script)
def reverse(self, podnapisi):
if podnapisi not in self.from_podnapisi:
raise LanguageReverseError(podnapisi)
return self.from_podnapisi[podnapisi]
+24
View File
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from babelfish import LanguageReverseConverter, language_converters
class TVsubtitlesConverter(LanguageReverseConverter):
def __init__(self):
self.alpha2_converter = language_converters['alpha2']
self.from_tvsubtitles = {'br': ('por', 'BR'), 'ua': ('ukr',), 'gr': ('ell',), 'cn': ('zho',), 'jp': ('jpn',),
'cz': ('ces',)}
self.to_tvsubtitles = set([(v, k) for k, v in self.from_tvsubtitles])
self.codes = self.alpha2_converter.codes | set(self.from_tvsubtitles.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3, country) in self.to_tvsubtitles:
return self.to_tvsubtitles[(alpha3, country)]
if (alpha3,) in self.to_tvsubtitles:
return self.to_tvsubtitles[(alpha3,)]
return self.alpha2_converter.convert(alpha3, country, script)
def reverse(self, tvsubtitles):
if tvsubtitles in self.from_tvsubtitles:
return self.from_tvsubtitles[tvsubtitles]
return self.alpha2_converter.reverse(tvsubtitles)
+27
View File
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
class Error(Exception):
"""Base class for exceptions in subliminal"""
pass
class ProviderError(Error):
"""Exception raised by providers"""
pass
class ProviderConfigurationError(ProviderError):
"""Exception raised by providers when badly configured"""
pass
class ProviderNotAvailable(ProviderError):
"""Exception raised by providers when unavailable"""
pass
class InvalidSubtitle(ProviderError):
"""Exception raised by providers when the downloaded subtitle is invalid"""
pass
-134
View File
@@ -1,134 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from BeautifulSoup import BeautifulSoup
from subliminal.classes import Subtitle
import guessit
import PluginBase
import urllib2
import re
class Addic7ed(PluginBase.PluginBase):
site_url = 'http://www.addic7ed.com'
site_name = 'Addic7ed'
server_url = 'http://www.addic7ed.com'
api_based = False
_plugin_languages = {u'English': 'en',
u'English (US)': 'en',
u'English (UK)': 'en',
u'Italian': 'it',
u'Portuguese': 'pt',
u'Portuguese (Brazilian)': 'pt-br',
u'Romanian': 'ro',
u'Español (Latinoamérica)': 'es',
u'Español (España)': 'es',
u'Spanish (Latin America)': 'es',
u'Español': 'es',
u'Spanish': 'es',
u'Spanish (Spain)': 'es',
u'French': 'fr',
u'Greek': 'el',
u'Arabic': 'ar',
u'German': 'de',
u'Croatian': 'hr',
u'Indonesian': 'id',
u'Hebrew': 'he',
u'Russian': 'ru',
u'Turkish': 'tr',
u'Swedish': 'se',
u'Czech': 'cs',
u'Dutch': 'nl',
u'Hungarian': 'hu',
u'Norwegian': 'no',
u'Polish': 'pl',
u'Persian': 'fa'}
def __init__(self, config_dict=None):
super(Addic7ed, self).__init__(self._plugin_languages, config_dict, isRevert=True)
#http://www.addic7ed.com/serie/Smallville/9/11/Absolute_Justice
self.release_pattern = re.compile(' \nVersion (.+), ([0-9]+).([0-9])+ MBs')
def list(self, filepath, languages):
if not self.checkLanguages(languages):
return []
guess = guessit.guess_file_info(filepath, 'autodetect')
if guess['type'] != 'episode':
self.logger.debug(u'Not an episode')
return []
# add multiple things to the release group set
release_group = set()
if 'releaseGroup' in guess:
release_group.add(guess['releaseGroup'].lower())
else:
if 'title' in guess:
release_group.add(guess['title'].lower())
if 'screenSize' in guess:
release_group.add(guess['screenSize'].lower())
if 'series' not in guess or len(release_group) == 0:
self.logger.debug(u'Not enough information to proceed')
return []
self.release_group = release_group # used to sort results
return self.query(guess['series'], guess['season'], guess['episodeNumber'], release_group, filepath, languages)
def query(self, name, season, episode, release_group, filepath, languages=None):
searchname = name.lower().replace(' ', '_')
if isinstance(searchname, unicode):
searchname = searchname.encode('utf-8')
searchurl = '%s/serie/%s/%s/%s/%s' % (self.server_url, urllib2.quote(searchname), season, episode, urllib2.quote(searchname))
self.logger.debug(u'Searching in %s' % searchurl)
try:
req = urllib2.Request(searchurl, headers={'User-Agent': self.user_agent})
page = urllib2.urlopen(req, timeout=self.timeout)
except urllib2.HTTPError as inst:
self.logger.info(u'Error: %s - %s' % (searchurl, inst))
return []
except urllib2.URLError as inst:
self.logger.info(u'TimeOut: %s' % inst)
return []
soup = BeautifulSoup(page.read())
sublinks = []
for html_sub in soup('td', {'class': 'NewsTitle', 'colspan': '3'}):
if not self.release_pattern.match(str(html_sub.contents[1])): # On not needed soup td result
continue
sub_teams = self.listTeams([self.release_pattern.match(str(html_sub.contents[1])).groups()[0].lower()], ['.', '_', ' ', '/', '-'])
if not release_group.intersection(sub_teams): # On wrong team
continue
html_language = html_sub.findNext('td', {'class': 'language'})
sub_language = self.getRevertLanguage(html_language.contents[0].strip().replace('&nbsp;', ''))
if languages and not sub_language in languages: # On wrong language
continue
html_status = html_language.findNextSibling('td')
sub_status = html_status.find('b').string.strip()
if not sub_status == 'Completed': # On not completed subtitles
continue
sub_link = self.server_url + html_status.findNextSibling('td', {'colspan': '3'}).find('a')['href']
self.logger.debug(u'Found a match with teams: %s' % sub_teams)
result = Subtitle(filepath, self.getSubtitlePath(filepath, sub_language), self.__class__.__name__, sub_language, sub_link, teams=sub_teams)
sublinks.append(result)
sublinks.sort(self._cmpReleaseGroup)
return sublinks
def download(self, subtitle):
self.downloadFile(subtitle.link, subtitle.path)
return subtitle
-154
View File
@@ -1,154 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from xml.dom import minidom
import guessit
import PluginBase
import os
try:
import cPickle as pickle
except ImportError:
import pickle
import urllib2
from subliminal.classes import Subtitle
class BierDopje(PluginBase.PluginBase):
site_url = 'http://bierdopje.com'
site_name = 'BierDopje'
server_url = 'http://api.bierdopje.com/A2B638AC5D804C2E/'
api_based = True
exceptions = {'the office': 10358,
'the office us': 10358,
'greys anatomy': 3733,
'sanctuary us': 7904,
'human target 2010': 12986,
'csi miami': 2187,
'castle 2009': 12708,
'chase 2010': 14228,
'the defenders 2010': 14225,
'hawaii five-0 2010': 14211}
_plugin_languages = {'en': 'en', 'nl': 'nl'}
def __init__(self, config_dict=None):
super(BierDopje, self).__init__(self._plugin_languages, config_dict)
#http://api.bierdopje.com/23459DC262C0A742/GetShowByName/30+Rock
#http://api.bierdopje.com/23459DC262C0A742/GetAllSubsFor/94/5/1/en (30 rock, season 5, episode 1)
if config_dict and config_dict['cache_dir']:
self.showid_cache = os.path.join(config_dict['cache_dir'], 'bierdopje_showid.cache')
with self.lock:
if not os.path.exists(self.showid_cache):
if not os.path.exists(os.path.dirname(self.showid_cache)):
raise Exception('Cache directory does not exist')
f = open(self.showid_cache, 'w')
pickle.dump({}, f)
f.close()
f = open(self.showid_cache, 'r')
self.showids = pickle.load(f)
self.logger.debug(u'Reading showids from cache: %s' % self.showids)
f.close()
def list(self, filepath, languages):
if not self.config_dict['cache_dir']:
raise Exception('Cache directory is required for this plugin')
possible_languages = self.possible_languages(languages)
if not possible_languages:
return []
guess = guessit.guess_file_info(filepath, 'autodetect')
if guess['type'] != 'episode':
self.logger.debug(u'Not an episode')
return []
# add multiple things to the release group set
release_group = set()
if 'releaseGroup' in guess:
release_group.add(guess['releaseGroup'].lower())
else:
if 'title' in guess:
release_group.add(guess['title'].lower())
if 'screenSize' in guess:
release_group.add(guess['screenSize'].lower())
if 'series' not in guess or len(release_group) == 0:
self.logger.debug(u'Not enough information to proceed')
return []
self.release_group = release_group # used to sort results
return self.query(guess['series'], guess['season'], guess['episodeNumber'], release_group, filepath, possible_languages)
def download(self, subtitle):
self.downloadFile(subtitle.link, subtitle.path)
return subtitle
def query(self, name, season, episode, release_group, filepath, languages):
sublinks = []
# get the show id
show_name = name.lower()
if show_name in self.exceptions: # get it from exceptions
show_id = self.exceptions[show_name]
elif show_name in self.showids: # get it from cache
show_id = self.showids[show_name]
else: # retrieve it
show_name_encoded = show_name
if isinstance(show_name_encoded, unicode):
show_name_encoded = show_name_encoded.encode('utf-8')
show_id_url = '%sGetShowByName/%s' % (self.server_url, urllib2.quote(show_name_encoded))
self.logger.debug(u'Retrieving show id from web at %s' % show_id_url)
page = urllib2.urlopen(show_id_url)
dom = minidom.parse(page)
if not dom or len(dom.getElementsByTagName('showid')) == 0: # no proper result
page.close()
return []
show_id = dom.getElementsByTagName('showid')[0].firstChild.data
self.showids[show_name] = show_id
with self.lock:
f = open(self.showid_cache, 'w')
self.logger.debug(u'Writing showid %s to cache file' % show_id)
pickle.dump(self.showids, f)
f.close()
page.close()
# get the subs for the show id we have
for language in languages:
subs_url = '%sGetAllSubsFor/%s/%s/%s/%s' % (self.server_url, show_id, season, episode, language)
self.logger.debug(u'Getting subtitles at %s' % subs_url)
page = urllib2.urlopen(subs_url)
dom = minidom.parse(page)
page.close()
for sub in dom.getElementsByTagName('result'):
sub_release = sub.getElementsByTagName('filename')[0].firstChild.data
if sub_release.endswith('.srt'):
sub_release = sub_release[:-4]
sub_release = sub_release + '.avi' # put a random extension for guessit not to fail guessing that file
# guess information from subtitle
sub_guess = guessit.guess_file_info(sub_release, 'episode')
sub_release_group = set()
if 'releaseGroup' in sub_guess:
sub_release_group.add(sub_guess['releaseGroup'].lower())
else:
if 'title' in sub_guess:
sub_release_group.add(sub_guess['title'].lower())
if 'screenSize' in sub_guess:
sub_release_group.add(sub_guess['screenSize'].lower())
sub_link = sub.getElementsByTagName('downloadlink')[0].firstChild.data
result = Subtitle(filepath, self.getSubtitlePath(filepath, language), self.__class__.__name__, language, sub_link, sub_release, sub_release_group)
sublinks.append(result)
sublinks.sort(self._cmpReleaseGroup)
return sublinks
-172
View File
@@ -1,172 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import PluginBase
import gzip
import os
import socket
import xmlrpclib
import guessit
import unicodedata
from subliminal.classes import Subtitle, DownloadFailedError
class OpenSubtitles(PluginBase.PluginBase):
site_url = 'http://www.opensubtitles.org'
site_name = 'OpenSubtitles'
server_url = 'http://api.opensubtitles.org/xml-rpc'
user_agent = 'Subliminal v0.4'
api_based = True
_plugin_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', 'pb': 'pob', 'un': 'unk', 'ay': 'ass'}
def __init__(self, config_dict=None):
super(OpenSubtitles, self).__init__(self._plugin_languages, config_dict)
def list(self, filepath, languages):
possible_languages = self.possible_languages(languages)
if not possible_languages:
return []
if os.path.isfile(filepath):
filehash = self.hashFile(filepath)
size = os.path.getsize(filepath)
return self.query(moviehash=filehash, languages=possible_languages, bytesize=size, filepath=filepath)
else:
return self.query(languages=possible_languages, filepath=filepath)
def download(self, subtitle):
try:
self.downloadFile(subtitle.link, subtitle.path + '.gz')
with open(subtitle.path, 'wb') as dump:
gz = gzip.open(subtitle.path + '.gz')
dump.write(gz.read())
gz.close()
self.adjustPermissions(subtitle.path)
os.remove(subtitle.path + '.gz')
except Exception as e:
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
raise DownloadFailedError(str(e))
return subtitle
def query(self, filepath, imdbID=None, moviehash=None, bytesize=None, languages=None):
"""Makes a query on OpenSubtitles and returns info about found subtitles.
Note: if using moviehash, bytesize is required. """
# prepare the search
search = {}
if moviehash:
search['moviehash'] = moviehash
if imdbID:
search['imdbid'] = imdbID
if bytesize:
search['moviebytesize'] = str(bytesize)
if languages:
search['sublanguageid'] = ','.join([self.getLanguage(l) for l in languages])
if not imdbID and not moviehash and not bytesize:
self.logger.debug(u'No search term, using the filename')
guess = guessit.guess_file_info(filepath, 'autodetect')
if guess['type'] == 'episode' and 'series' in guess:
search['query'] = guess['series'].lower()
elif guess['type'] == 'movie':
search['query'] = guess['title'].lower()
else: # we don't know what we have
return[]
# login
self.server = xmlrpclib.Server(self.server_url)
socket.setdefaulttimeout(self.timeout)
try:
log_result = self.server.LogIn('', '', 'eng', self.user_agent)
if not log_result['status'] or log_result['status'] != '200 OK' or not log_result['token']:
raise Exception('OpenSubtitles login failed')
token = log_result['token']
except Exception:
self.logger.error(u'Cannot login')
token = None
socket.setdefaulttimeout(None)
return []
# search
sublinks = self.get_results(token, search, filepath)
# logout
try:
self.server.LogOut(token)
except:
self.logger.error(u'Cannot logout')
socket.setdefaulttimeout(None)
return sublinks
def get_results(self, token, search, filepath):
self.logger.debug(u'Query uses token %s and search parameters %s' % (token, search))
try:
results = self.server.SearchSubtitles(token, [search])
except Exception:
self.logger.debug(u'Cannot query the server')
return []
if not results['data']: # no subtitle found
return []
sublinks = []
self.filename = self.getFileName(filepath)
for r in sorted(results['data'], self._cmpSubFileName):
result = Subtitle(filepath, self.getSubtitlePath(filepath, self.getRevertLanguage(r['SubLanguageID'])), self.__class__.__name__, self.getRevertLanguage(r['SubLanguageID']), r['SubDownloadLink'], r['SubFileName'])
if 'query' in search: # query mode search, filter results
query_encoded = search['query']
if isinstance(query_encoded, unicode):
query_encoded = unicodedata.normalize('NFKD', query_encoded).encode('ascii', 'ignore')
if not r['MovieReleaseName'].replace('.', ' ').lower().startswith(query_encoded):
self.logger.debug(u'Skipping %s it does not start with %s' % (r['MovieReleaseName'].replace('.', ' ').lower(), query_encoded))
continue
sublinks.append(result)
return sublinks
def _cmpSubFileName(self, x, y):
"""Sort based on the SubFileName name tag """
#TODO add also support for subtitles release
xmatch = x['SubFileName'] and (x['SubFileName'].find(self.filename) > -1 or self.filename.find(x['SubFileName']) > -1)
ymatch = y['SubFileName'] and (y['SubFileName'].find(self.filename) > -1 or self.filename.find(y['SubFileName']) > -1)
if xmatch and ymatch:
if x['SubFileName'] == self.filename or x['SubFileName'].startswith(self.filename):
return - 1
return 0
if not xmatch and not ymatch:
return 0
if xmatch and not ymatch:
return - 1
if not xmatch and ymatch:
return 1
return 0
-163
View File
@@ -1,163 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import abc
import logging
import os
import urllib2
import struct
import threading
import socket
from subliminal.classes import DownloadFailedError
class PluginBase(object):
__metaclass__ = abc.ABCMeta
api_based = True
timeout = 3
user_agent = 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.3)'
lock = threading.Lock()
@abc.abstractmethod
def __init__(self, pluginLanguages, config_dict=None, isRevert=False):
self.config_dict = config_dict
if not pluginLanguages:
self.pluginLanguages = None
self.revertPluginLanguages = None
elif not isRevert:
self.pluginLanguages = pluginLanguages
self.revertPluginLanguages = dict((v, k) for k, v in self.pluginLanguages.iteritems())
else:
self.revertPluginLanguages = pluginLanguages
self.pluginLanguages = dict((v, k) for k, v in self.revertPluginLanguages.iteritems())
self.logger = logging.getLogger('subliminal.%s' % self.__class__.__name__)
@staticmethod
def getFileName(filepath):
filename = filepath
if os.path.isfile(filename):
filename = os.path.basename(filename)
if filename.endswith(('.avi', '.wmv', '.mov', '.mp4', '.mpeg', '.mpg', '.mkv')):
filename = filename.rsplit('.', 1)[0]
return filename
def possible_languages(self, languages):
possible_languages = languages & set(self.pluginLanguages.keys())
if not possible_languages:
self.logger.debug(u'Languages %r are not in supported languages' % languages)
return possible_languages
def hashFile(self, filename):
"""Hash a file like OpenSubtitles"""
longlongformat = 'q' # long long
bytesize = struct.calcsize(longlongformat)
f = open(filename, 'rb')
filesize = os.path.getsize(filename)
hash = filesize
if filesize < 65536 * 2:
self.logger.error(u'File %s is too small (SizeError < 2**16)' % filename)
return []
for _ in range(65536 / bytesize):
buffer = f.read(bytesize)
(l_value,) = struct.unpack(longlongformat, buffer)
hash += l_value
hash = hash & 0xFFFFFFFFFFFFFFFF # to remain as 64bit number
f.seek(max(0, filesize - 65536), 0)
for _ in range(65536 / bytesize):
buffer = f.read(bytesize)
(l_value,) = struct.unpack(longlongformat, buffer)
hash += l_value
hash = hash & 0xFFFFFFFFFFFFFFFF
f.close()
returnedhash = '%016x' % hash
return returnedhash
def downloadFile(self, url, filepath, data=None):
"""Download a subtitle file"""
self.logger.info(u'Downloading %s' % url)
socket.setdefaulttimeout(self.timeout)
try:
req = urllib2.Request(url, headers={'Referer': url, 'User-Agent': self.user_agent})
with open(filepath, 'wb') as dump:
f = urllib2.urlopen(req, data=data)
dump.write(f.read())
self.adjustPermissions(filepath)
f.close()
except Exception as e:
self.logger.error(u'Download %s failed: %s' % (url, e))
if os.path.exists(filepath):
os.remove(filepath)
raise DownloadFailedError(str(e))
finally:
socket.setdefaulttimeout(self.timeout)
self.logger.debug(u'Download finished for file %s. Size: %s' % (filepath, os.path.getsize(filepath)))
def adjustPermissions(self, filepath):
if self.config_dict and 'files_mode' in self.config_dict and self.config_dict['files_mode'] != -1:
os.chmod(filepath, self.config_dict['files_mode'])
@abc.abstractmethod
def list(self, filepath, languages):
"""List subtitles"""
@abc.abstractmethod
def download(self, subtitle):
"""Download a subtitle"""
def getRevertLanguage(self, language):
"""ISO-639-1 language code from plugin language code"""
try:
return self.revertPluginLanguages[language]
except KeyError:
self.logger.warn(u'Ooops, you found a missing language in the configuration file of %s: %s. Send a bug report to have it added.' % (self.__class__.__name__, language))
def getLanguage(self, language):
"""Plugin language code from ISO-639-1 language code"""
try:
return self.pluginLanguages[language]
except KeyError:
self.logger.warn(u'Ooops, you found a missing language in the configuration file of %s: %s. Send a bug report to have it added.' % (self.__class__.__name__, language))
def getSubtitlePath(self, video_path, language):
if not os.path.exists(video_path):
video_path = os.path.split(video_path)[1]
path = video_path.rsplit('.', 1)[0]
if self.config_dict and self.config_dict['multi']:
return path + '.%s.srt' % language
return path + '.srt'
def listTeams(self, sub_teams, separators):
"""List teams of a given string using separators"""
for sep in separators:
sub_teams = self.splitTeam(sub_teams, sep)
return set(sub_teams)
def splitTeam(self, sub_teams, sep):
"""Split teams of a given string using separators"""
teams = []
for t in sub_teams:
teams += t.split(sep)
return teams
def _cmpReleaseGroup(self, x, y):
"""Sort based on teams matching"""
return -cmp(len(x.teams.intersection(self.release_group)), len(y.teams.intersection(self.release_group)))
-135
View File
@@ -1,135 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from hashlib import md5, sha256
import PluginBase
import xmlrpclib
import socket
import os
class Podnapisi(PluginBase.PluginBase):
site_url = "http://www.podnapisi.net"
site_name = "Podnapisi"
server_url = 'http://ssp.podnapisi.net:8000'
api_based = True
_plugin_languages = {"sl": "1",
"en": "2",
"no": "3",
"ko": "4",
"de": "5",
"is": "6",
"cs": "7",
"fr": "8",
"it": "9",
"bs": "10",
"ja": "11",
"ar": "12",
"ro": "13",
"es-ar": "14",
"hu": "15",
"el": "16",
"zh": "17",
"lt": "19",
"et": "20",
"lv": "21",
"he": "22",
"nl": "23",
"da": "24",
"se": "25",
"pl": "26",
"ru": "27",
"es": "28",
"sq": "29",
"tr": "30",
"fi": "31",
"pt": "32",
"bg": "33",
"mk": "35",
"sk": "37",
"hr": "38",
"zh": "40",
"hi": "42",
"th": "44",
"uk": "46",
"sr": "47",
"pt-br": "48",
"ga": "49",
"be": "50",
"vi": "51",
"fa": "52",
"ca": "53",
"id": "54"}
def __init__(self, config_dict=None):
super(Podnapisi, self).__init__(self._plugin_languages, config_dict)
# Podnapisi uses two reference for latin serbian and cyrillic serbian (36 and 47)
# add the 36 manually as cyrillic seems to be more used
self.revertPluginLanguages["36"] = "sr"
def list(self, filenames, languages):
"""Main method to call when you want to list subtitles"""
filepath = filenames[0]
if not os.path.isfile(filepath):
return []
return self.query(self.hashFile(filepath), languages)
def download(self, subtitle):
return []
def query(self, moviehash, languages=None):
"""Makes a query on podnapisi and returns info (link, lang) about found subtitles"""
# login
self.server = xmlrpclib.Server(self.server_url)
socket.setdefaulttimeout(self.timeout)
try:
log_result = self.server.initiate(self.user_agent)
self.logger.debug(u"Result: %s" % log_result)
token = log_result["session"]
nonce = log_result["nonce"]
except Exception:
self.logger.error(u"Cannot login" % log_result)
socket.setdefaulttimeout(None)
return []
username = 'getmesubs'
password = '99D31$$'
hash = md5()
hash.update(password)
password = hash.hexdigest()
hash = sha256()
hash.update(password)
hash.update(nonce)
password = hash.hexdigest()
self.server.authenticate(token, username, password)
self.logger.debug(u'Authenticated')
#if languages:
# self.logger.debug([self.getLanguage(l) for l in languages])
# self.server.setFilters(token, [self.getLanguage(l) for l in languages])
# self.logger.debug('Filers set for languages %s' % languages)
self.logger.debug(u"Starting search with token %s and hashs %s" % (token, [moviehash]))
results = self.server.search(token, [moviehash])
return results
subs = []
for sub in results['results']:
subs.append(sub)
self.server.terminate(token)
return subs
-168
View File
@@ -1,168 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from BeautifulSoup import BeautifulSoup
import PluginBase
import zipfile
import os
import urllib2
class SubScene(PluginBase.PluginBase):
site_url = 'http://subscene.com'
site_name = 'SubScene'
server_url = 'http://subscene.com/s.aspx?subtitle='
api_based = False
_plugin_languages = {"en": "English",
"se": "Swedish",
"da": "Danish",
"fi": "Finnish",
"no": "Norwegian",
"fr": "French",
"es": "Spanish",
"is": "Icelandic",
"cs": "Czech",
"bg": "Bulgarian",
"de": "German",
"ar": "Arabic",
"el": "Greek",
"fa": "Farsi/Persian",
"nl": "Dutch",
"he": "Hebrew",
"id": "Indonesian",
"ja": "Japanese",
"vi": "Vietnamese",
"pt": "Portuguese",
"ro": "Romanian",
"tr": "Turkish",
"sr": "Serbian",
"pt-br": "Brazillian Portuguese",
"ru": "Russian",
"hr": "Croatian",
"sl": "Slovenian",
"zh": "Chinese BG code",
"it": "Italian",
"pl": "Polish",
"ko": "Korean",
"hu": "Hungarian",
"ku": "Kurdish",
"et": "Estonian"}
def __init__(self, config_dict=None):
super(SubScene, self).__init__(self._plugin_languages, config_dict)
#http://subscene.com/s.aspx?subtitle=Dexter.S04E01.HDTV.XviD-NoTV
def list(self, filenames, languages):
"""Main method to call when you want to list subtitles"""
filepath = filenames[0]
fname = self.getFileName(filepath)
subs = self.query(fname, filepath, languages)
if not subs and fname.rfind(".[") > 0:
# Try to remove the [VTV] or [EZTV] at the end of the file
teamless_filename = fname[0:fname.rfind(".[")]
subs = self.query(teamless_filename, filepath, languages)
return subs
else:
return subs
def download(self, subtitle):
"""Main method to call when you want to download a subtitle"""
subpage = subtitle["page"]
page = urllib2.urlopen(subpage)
soup = BeautifulSoup(page)
dlhref = soup.find("div", {"class": "download"}).find("a")["href"]
subtitle["link"] = self.site_url + dlhref.split('"')[7]
format = "zip"
archivefilename = subtitle["filename"].rsplit(".", 1)[0] + '.' + format
self.downloadFile(subtitle["link"], archivefilename)
subtitlefilename = None
if zipfile.is_zipfile(archivefilename):
self.logger.debug(u"Unzipping file " + archivefilename)
zf = zipfile.ZipFile(archivefilename, "r")
for el in zf.infolist():
extension = el.orig_filename.rsplit(".", 1)[1]
if extension in ("srt", "sub", "txt"):
subtitlefilename = srtbasefilename + "." + extension
outfile = open(subtitlefilename, "wb")
outfile.write(zf.read(el.orig_filename))
outfile.flush()
self.adjustPermissions(subtitlefilename)
outfile.close()
else:
self.logger.info(u"File %s does not seem to be valid " % el.orig_filename)
# Deleting the zip file
zf.close()
os.remove(archivefilename)
return subtitlefilename
elif archivefilename.endswith('.rar'):
self.logger.warn(u'Rar is not really supported yet. Trying to call unrar')
import subprocess
try:
args = ['unrar', 'lb', archivefilename]
output = subprocess.Popen(args, stdout=subprocess.PIPE).communicate()[0]
for el in output.splitlines():
extension = el.rsplit(".", 1)[1]
if extension in ("srt", "sub"):
args = ['unrar', 'e', archivefilename, el, os.path.dirname(archivefilename)]
subprocess.Popen(args)
tmpsubtitlefilename = os.path.join(os.path.dirname(archivefilename), el)
subtitlefilename = os.path.join(os.path.dirname(archivefilename), srtbasefilename + "." + extension)
if os.path.exists(tmpsubtitlefilename):
# rename it to match the file
os.rename(tmpsubtitlefilename, subtitlefilename)
# exit
return subtitlefilename
except OSError as e:
self.logger.error(u"Execution failed: %s" % e)
return None
else:
self.logger.info(u"Unexpected file type (not zip) for %s" % archivefilename)
return None
def downloadFile(self, url, filename):
"""Downloads the given url to the given filename"""
#FIXME: Not working
def query(self, token, filepath, langs=None):
"""Make a query on SubScene and returns info about found subtitles"""
sublinks = []
searchurl = "%s%s" % (self.server_url, urllib2.quote(token))
self.logger.debug(u"Query: %s" % searchurl)
page = urllib2.urlopen(searchurl)
soup = BeautifulSoup(page.read())
for subs in soup("a", {"class": "a1"}):
lang_span = subs.find("span")
lang = self.getRevertLanguage(lang_span.contents[0].strip())
release_span = lang_span.findNext("span")
release = release_span.contents[0].strip().split(" (")[0]
sub_page = subs["href"]
#http://subscene.com//s-dlpath-260016/78348/rar.zipx
if release.lower().startswith(token.lower()) and (not langs or lang in langs):
result = {}
result["release"] = release
result["lang"] = lang
result["link"] = None
result["page"] = self.site_url + sub_page
result["filename"] = filepath
result["plugin"] = self.__class__.__name__
sublinks.append(result)
return sublinks
-116
View File
@@ -1,116 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from BeautifulSoup import BeautifulSoup
import PluginBase
import urllib2
import re
import guessit
from subliminal.classes import Subtitle
class SubsWiki(PluginBase.PluginBase):
site_url = 'http://www.subswiki.com'
site_name = 'SubsWiki'
server_url = 'http://www.subswiki.com'
api_based = False
_plugin_languages = {u'English (US)': 'en',
u'English (UK)': 'en',
u'English': 'en',
u'French': 'fr',
u'Brazilian': 'pt-br',
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'}
def __init__(self, config_dict=None):
super(SubsWiki, self).__init__(self._plugin_languages, config_dict, True)
self.release_pattern = re.compile('\nVersion (.+), ([0-9]+).([0-9])+ MBs')
def list(self, filepath, languages):
possible_languages = self.possible_languages(languages)
if not possible_languages:
return []
guess = guessit.guess_file_info(filepath, 'autodetect')
if guess['type'] != 'episode':
self.logger.debug(u'Not an episode')
return []
# add multiple things to the release group set
release_group = set()
if 'releaseGroup' in guess:
release_group.add(guess['releaseGroup'].lower())
else:
if 'title' in guess:
release_group.add(guess['title'].lower())
if 'screenSize' in guess:
release_group.add(guess['screenSize'].lower())
if 'series' not in guess or len(release_group) == 0:
self.logger.debug(u'Not enough information to proceed')
return []
self.release_group = release_group # used to sort results
return self.query(guess['series'], guess['season'], guess['episodeNumber'], release_group, filepath, possible_languages)
def query(self, name, season, episode, release_group, filepath, languages):
sublinks = []
searchname = name.lower().replace(' ', '_')
if isinstance(searchname, unicode):
searchname = searchname.encode('utf-8')
searchurl = '%s/serie/%s/%s/%s/' % (self.server_url, urllib2.quote(searchname), season, episode)
self.logger.debug(u'Searching in %s' % searchurl)
try:
req = urllib2.Request(searchurl, headers={'User-Agent': self.user_agent})
page = urllib2.urlopen(req, timeout=self.timeout)
except urllib2.HTTPError as inst:
self.logger.info(u'Error: %s - %s' % (searchurl, inst))
return []
except urllib2.URLError as inst:
self.logger.info(u'TimeOut: %s' % inst)
return []
soup = BeautifulSoup(page.read())
for subs in soup('td', {'class': 'NewsTitle'}):
sub_teams = self.listTeams([self.release_pattern.search('%s' % subs.contents[1]).group(1).lower()], ['.', '_', ' ', '/', '-'])
if not release_group.intersection(sub_teams): # On wrong team
continue
self.logger.debug(u'Team from website: %s' % sub_teams)
self.logger.debug(u'Team from file: %s' % release_group)
for html_language in subs.parent.parent.findAll('td', {'class': 'language'}):
sub_language = self.getRevertLanguage(html_language.string.strip())
self.logger.debug(u'Subtitle reverted language: %s' % sub_language)
if not sub_language in languages: # On wrong language
continue
html_status = html_language.findNextSibling('td')
sub_status = html_status.find('strong').string.strip()
if not sub_status == 'Completed': # On not completed subtitles
continue
sub_link = html_status.findNext('td').find('a')['href']
result = Subtitle(filepath, self.getSubtitlePath(filepath, sub_language), self.__class__.__name__, sub_language, self.server_url + sub_link, teams=sub_teams)
sublinks.append(result)
sublinks.sort(self._cmpReleaseGroup)
return sublinks
def download(self, subtitle):
self.downloadFile(subtitle.link, subtitle.path)
return subtitle
-109
View File
@@ -1,109 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from BeautifulSoup import BeautifulSoup
import guessit
import urllib2
import unicodedata
import re
import PluginBase
from subliminal.classes import Subtitle
class Subtitulos(PluginBase.PluginBase):
site_url = 'http://www.subtitulos.es'
site_name = 'Subtitulos'
server_url = 'http://www.subtitulos.es'
api_based = False
_plugin_languages = {u'English (US)': 'en', u'English (UK)': 'en', u'English': 'en', u'French': 'fr', u'Brazilian': 'pt-br',
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'}
def __init__(self, config_dict=None):
super(Subtitulos, self).__init__(self._plugin_languages, config_dict, True)
self.release_pattern = re.compile('Versi&oacute;n (.+) ([0-9]+).([0-9])+ megabytes')
def list(self, filepath, languages):
possible_languages = self.possible_languages(languages)
if not possible_languages:
return []
guess = guessit.guess_file_info(filepath, 'autodetect')
if guess['type'] != 'episode':
self.logger.debug(u'Not an episode')
return []
# add multiple things to the release group set
release_group = set()
if 'releaseGroup' in guess:
release_group.add(guess['releaseGroup'].lower())
else:
if 'title' in guess:
release_group.add(guess['title'].lower())
if 'screenSize' in guess:
release_group.add(guess['screenSize'].lower())
if 'series' not in guess or len(release_group) == 0:
self.logger.debug(u'Not enough information to proceed')
return []
self.release_group = release_group # used to sort results
return self.query(guess['series'], guess['season'], guess['episodeNumber'], release_group, filepath, possible_languages)
def query(self, name, season, episode, release_group, filepath, languages):
sublinks = []
searchname = name.lower().replace(' ', '-')
if isinstance(searchname, unicode):
searchname = unicodedata.normalize('NFKD', searchname).encode('ascii','ignore')
searchurl = '%s/%s/%sx%.2d' % (self.server_url, urllib2.quote(searchname), season, episode)
self.logger.debug(u'Searching in %s' % searchurl)
try:
req = urllib2.Request(searchurl, headers={'User-Agent': self.user_agent})
page = urllib2.urlopen(req, timeout=self.timeout)
except urllib2.HTTPError as inst:
self.logger.info(u'Error: %s - %s' % (searchurl, inst))
return []
except urllib2.URLError as inst:
self.logger.info(u'TimeOut: %s' % inst)
return []
soup = BeautifulSoup(page.read())
for subs in soup('div', {'id': 'version'}):
version = subs.find('p', {'class': 'title-sub'})
sub_teams = self.listTeams([self.release_pattern.search('%s' % version.contents[1]).group(1).lower()], ['.', '_', ' ', '/', '-'])
self.logger.debug(u'Team from website: %s' % sub_teams)
self.logger.debug(u'Team from file: %s' % release_group)
if not release_group.intersection(sub_teams): # On wrong team
continue
for html_language in subs.findAllNext('ul', {'class': 'sslist'}):
sub_language = self.getRevertLanguage(html_language.findNext('li', {'class': 'li-idioma'}).find('strong').contents[0].string.strip())
if not sub_language in languages: # On wrong language
continue
html_status = html_language.findNext('li', {'class': 'li-estado green'})
sub_status = html_status.contents[0].string.strip()
if not sub_status == 'Completado': # On not completed subtitles
continue
sub_link = html_status.findNext('span', {'class': 'descargar green'}).find('a')['href']
result = Subtitle(filepath, self.getSubtitlePath(filepath, sub_language), self.__class__.__name__, sub_language, sub_link, teams=sub_teams)
sublinks.append(result)
sublinks.sort(self._cmpReleaseGroup)
return sublinks
def download(self, subtitle):
self.downloadFile(subtitle.link, subtitle.path)
return subtitle
-87
View File
@@ -1,87 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import PluginBase
import hashlib
import os
import urllib2
from subliminal.classes import Subtitle
class TheSubDB(PluginBase.PluginBase):
site_url = 'http://thesubdb.com'
site_name = 'SubDB'
server_url = 'http://api.thesubdb.com' # for testing purpose, use http://sandbox.thesubdb.com instead
api_based = True
user_agent = 'SubDB/1.0 (Subliminal/0.4; https://github.com/Diaoul/subliminal)' # defined by the API
_plugin_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
def __init__(self, config_dict=None):
super(TheSubDB, self).__init__(self._plugin_languages, config_dict)
def list(self, filepath, languages):
possible_languages = self.possible_languages(languages)
if not possible_languages:
return []
if not os.path.isfile(filepath):
return []
return self.query(filepath, self.hashFile(filepath), possible_languages)
def query(self, filepath, moviehash, languages):
searchurl = '%s/?action=%s&hash=%s' % (self.server_url, 'search', moviehash)
self.logger.debug(u'Query URL: %s' % searchurl)
try:
req = urllib2.Request(searchurl, headers={'User-Agent': self.user_agent})
page = urllib2.urlopen(req, timeout=self.timeout)
except urllib2.HTTPError as inst:
if inst.code == 404: # no result found
return []
self.logger.error(u'Error: %s - %s' % (searchurl, inst))
return []
except urllib2.URLError as inst:
self.logger.error(u'TimeOut: %s' % inst)
return []
available_languages = page.readlines()[0].split(',')
self.logger.debug(u'Available languages: %s' % available_languages)
subs = []
for l in available_languages:
if l in languages:
result = Subtitle(filepath, self.getSubtitlePath(filepath, l), self.__class__.__name__, l, '%s/?action=download&hash=%s&language=%s' % (self.server_url, moviehash, l))
subs.append(result)
return subs
def hashFile(self, filepath):
"""TheSubDB specific hash function"""
readsize = 64 * 1024
with open(filepath, 'rb') as f:
data = f.read(readsize)
f.seek(-readsize, os.SEEK_END)
data += f.read(readsize)
return hashlib.md5(data).hexdigest()
def download(self, subtitle):
self.downloadFile(subtitle.link, subtitle.path)
return subtitle
-31
View File
@@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#from Addic7ed import Addic7ed
from BierDopje import BierDopje
from OpenSubtitles import OpenSubtitles
#from Podnapisi import Podnapisi
#from SubScene import SubScene
from SubsWiki import SubsWiki
from Subtitulos import Subtitulos
from TheSubDB import TheSubDB
+156
View File
@@ -0,0 +1,156 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import babelfish
from ..video import Episode, Movie
from .. import __version__
from random import randint
# Agent List
AGENT_LIST = (
'Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0',
'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10; rv:33.0) Gecko/20100101 Firefox/33.0',
'Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/31.0',
'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:31.0) Gecko/20130401 Firefox/31.0',
'Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36',
'Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko',
'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A',
'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25',
)
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
# Returns a random agent to use from the list above
random_user_agent = AGENT_LIST[randint(0, len(AGENT_LIST)-1)]
# Defines the ideal user agent to use for all providers otherwise
primary_user_agent = 'Subliminal/%s' % __version__
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)
+166
View File
@@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import babelfish
import bs4
import charade
import requests
from . import Provider
from ..cache import region
from ..exceptions import ProviderConfigurationError, ProviderNotAvailable, InvalidSubtitle
from ..subtitle import Subtitle, is_valid_subtitle, sanitize_string
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 = set([babelfish.Language('por', 'BR')]) | set([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 initialize(self):
self.session = requests.Session()
self.session.headers = {
'User-Agent': self.random_user_agent,
'Referer': self.server,
}
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[sanitize_string(html_show.string)] = \
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()
sanitized_series = sanitize_string(series)
if sanitized_series in show_ids:
show_id = show_ids[sanitized_series]
else:
show_id = self.find_show_id(sanitized_series)
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
if not cells[3].string:
logger.debug('Skipping empty language')
continue
subtitles.append(Addic7edSubtitle(babelfish.Language.fromaddic7ed(cells[3].string), series, season,
int(cells[1].string), cells[2].string, cells[4].string,
bool(cells[6].string), cells[9].a['href'], link))
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
+164
View File
@@ -0,0 +1,164 @@
# -*- 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 ..subtitle import sanitize_string
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, # @ReservedAssignment
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 \
sanitize_string(self.series_name) == \
sanitize_string(video.series):
matches.add('series')
# season
if video.season and self.series_season == video.season:
matches.add('season')
# episode
if video.episode and self.series_episode == video.episode:
matches.add('episode')
# 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 \
sanitize_string(self.movie_name) == \
sanitize_string(video.title):
matches.add('title')
return matches
class OpenSubtitlesProvider(Provider):
languages = set([babelfish.Language.fromopensubtitles(l) for l in babelfish.language_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): # @ReservedAssignment
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 []
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
+208
View File
@@ -0,0 +1,208 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import io
import logging
import re
import contextlib
import xml.etree.ElementTree
import zipfile
import babelfish
import bs4
import charade
import guessit
import requests
from . import Provider
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
from ..subtitle import Subtitle, is_valid_subtitle, compute_guess_matches
from ..subtitle import sanitize_string
from ..video import Episode, Movie
logger = logging.getLogger(__name__)
URL_RE = re.compile(
'^((http[s]?|ftp):\/)?\/?([^:\/\s]+)(:([^\/]*))?((\/\w+)*\/)' + \
'([\w\-\.]+[^#?\s]+)(\?([^#]*))?(#(.*))?$',
)
class PodnapisiSubtitle(Subtitle):
provider_name = 'podnapisi'
def __init__(self, language, id, releases, hearing_impaired, link, series=None, season=None, episode=None, # @ReservedAssignment
title=None, year=None):
super(PodnapisiSubtitle, self).__init__(language, hearing_impaired)
self.id = id
self.releases = releases
self.hearing_impaired = hearing_impaired
self.link = '/ppodnapisi' + link
self.series = series
self.season = season
self.episode = episode
self.title = title
self.year = year
def compute_matches(self, video):
matches = set()
# episode
if isinstance(video, Episode):
# series
if video.series and \
sanitize_string(self.series) == \
sanitize_string(video.series):
matches.add('series')
# season
if video.season and self.season == video.season:
matches.add('season')
# episode
if video.episode and self.episode == video.episode:
matches.add('episode')
# guess
for release in self.releases:
matches |= compute_guess_matches(video, guessit.guess_episode_info(release + '.mkv'))
# movie
elif isinstance(video, Movie):
# title
if video.title and \
sanitize_string(self.title) == \
sanitize_string(video.title):
matches.add('title')
# year
if video.year and self.year == video.year:
matches.add('year')
# guess
for release in self.releases:
matches |= compute_guess_matches(video, guessit.guess_movie_info(release + '.mkv'))
return matches
class PodnapisiProvider(Provider):
languages = set([babelfish.Language.frompodnapisi(l) for l in babelfish.language_converters['podnapisi'].codes])
video_types = (Episode, Movie)
server = 'http://simple.podnapisi.net'
pre_link_re = re.compile('^.*(?P<link>/ppodnapisi/predownload/i/\d+/k/.*$)')
link_re = re.compile('^.*(?P<link>/[a-zA-Z]{2}/ppodnapisi/download/i/\d+/k/.*$)')
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': self.primary_user_agent }
def terminate(self):
self.session.close()
def get(self, url, params=None, headers=None, is_xml=True):
"""Make a GET request on `url` with the given parameters
:param string url: part of the URL to reach with the leading slash
:param dict params: params of the request
:param dict headers: headers of the request
:param bool xml: whether the response content is XML or not
:return: the response
:rtype: :class:`xml.etree.ElementTree.Element` or :class:`bs4.BeautifulSoup`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
prefix_url = ''
url_result = URL_RE.search(url)
if url_result and url_result.group(2) is None:
prefix_url = self.server
try:
r = self.session.get(
prefix_url + url, params=params,
headers=headers,
timeout=10,
)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
if is_xml:
return xml.etree.ElementTree.fromstring(r.content)
else:
return bs4.BeautifulSoup(r.content, ['permissive'])
def query(self, language, series=None, season=None, episode=None, title=None, year=None):
params = {'sXML': 1, 'sJ': language.podnapisi}
if series and season and episode:
params['sK'] = series
params['sTS'] = season
params['sTE'] = episode
elif title:
params['sK'] = title
if year:
params['sY'] = year
else:
raise ValueError('Missing parameters series and season and episode or title')
logger.debug('Searching episode %r', params)
subtitles = []
while True:
root = self.get('/ppodnapisi/search', params)
if not int(root.find('pagination/results').text):
logger.debug('No subtitle found')
break
if series and season and episode:
try:
subtitles.extend([PodnapisiSubtitle(language, int(s.find('id').text), s.find('release').text.split(),
'h' in (s.find('flags').text or ''), s.find('url').text[38:],
series=series, season=season, episode=episode)
for s in root.findall('subtitle')])
except AttributeError:
# there simply wasn't enough information in the TV Show
# gracefully handle this instead of crashing :)
break
elif title:
try:
subtitles.extend([PodnapisiSubtitle(language, int(s.find('id').text), s.find('release').text.split(),
'h' in (s.find('flags').text or ''), s.find('url').text[38:],
title=title, year=year)
for s in root.findall('subtitle')])
except AttributeError:
# there simply wasn't enough information in the movie
# gracefully handle this instead of crashing :)
break
if int(root.find('pagination/current').text) >= int(root.find('pagination/count').text):
break
params['page'] = int(root.find('pagination/current').text) + 1
return subtitles
def list_subtitles(self, video, languages):
if isinstance(video, Episode):
return [s for l in languages for s in self.query(l, series=video.series, season=video.season,
episode=video.episode)]
elif isinstance(video, Movie):
return [s for l in languages for s in self.query(l, title=video.title, year=video.year)]
def download_subtitle(self, subtitle):
soup = self.get(subtitle.link, is_xml=False)
pre_link = soup.find('a', href=self.pre_link_re)
if not pre_link:
raise ProviderError('Cannot find the pre-download link')
pre_link = self.server + \
self.pre_link_re.match(pre_link['href']).group('link')
# Continue following the link
soup = self.get(
pre_link,
headers={
'Referer': self.server,
},
is_xml=False,
)
link = soup.find('a', href=self.link_re)
if not link:
raise ProviderError('Cannot find the download link')
try:
r = self.session.get(self.server + self.link_re.match(link['href']).group('link'), timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
with contextlib.closing(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
+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): # @ReservedAssignment
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 = set([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): # @ReservedAssignment
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
set([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
+195
View File
@@ -0,0 +1,195 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import io
import logging
import re
import contextlib
import zipfile
import babelfish
import bs4
import charade
import requests
from . import Provider
from ..cache import region
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
from ..subtitle import Subtitle, is_valid_subtitle, sanitize_string
from ..video import Episode
IGNORE_DATEMATCH=re.compile('^(.*)[ \t0-9-._)(]*$')
logger = logging.getLogger(__name__)
class TVsubtitlesSubtitle(Subtitle):
provider_name = 'tvsubtitles'
def __init__(self, language, series, season, episode, id, rip, release): # @ReservedAssignment
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 = set([babelfish.Language('por', 'BR')]) | set([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$')
link_re = re.compile('^(?P<series>.+) \(\d{4}-\d{4}\)$')
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': self.primary_user_agent }
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
:param string method: method 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-"]')
sanitized_series = IGNORE_DATEMATCH.match(
sanitize_string(series).replace('.', ' ').strip(),
)
if not sanitized_series:
sanitized_series = sanitize_string(series)\
.replace('.', ' ').strip()
else:
sanitized_series = sanitized_series.group(1)
if not links:
logger.info('Series %r not found', series)
return None
for link in links:
match = self.link_re.match(link.string)
if not match:
logger.warning('Could not parse %r', link.string)
continue
show = IGNORE_DATEMATCH.match(
sanitize_string(match.group('series'))\
.replace('.', ' ').strip(),
)
if not show:
logger.warning('Could not postparse %r', match.group('series'))
continue
show = show.group(1)
if show == sanitized_series:
return int(link['href'][8:-5])
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)
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 contextlib.closing(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') # @ReservedAssignment
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]))
-325
View File
@@ -1,325 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import threading
from itertools import groupby
from classes import DownloadTask, ListTask, StopTask, LanguageError, PluginError, BadStateError, WrongTaskError, DownloadFailedError
import Queue
import logging
import mimetypes
import os
import plugins
# be nice
try:
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record):
pass
logger = logging.getLogger('subliminal')
logger.addHandler(NullHandler())
# const
FORMATS = ['video/x-msvideo', 'video/quicktime', 'video/x-matroska', 'video/mp4']
EXTENSIONS = set(['srt', 'sub'])
LANGUAGES = set(['aa', 'ab', 'ae', 'af', 'ak', 'am', 'an', 'ar', 'as', 'av', 'ay', 'az', 'ba', 'be', 'bg', 'bh', 'bi',
'bm', 'bn', 'bo', 'br', 'bs', 'ca', 'ce', 'ch', 'co', 'cr', 'cs', 'cu', 'cv', 'cy', 'da', 'de', 'dv',
'dz', 'ee', 'el', 'en', 'eo', 'es', 'et', 'eu', 'fa', 'ff', 'fi', 'fj', 'fo', 'fr', 'fy', 'ga', 'gd',
'gl', 'gn', 'gu', 'gv', 'ha', 'he', 'hi', 'ho', 'hr', 'ht', 'hu', 'hy', 'hz', 'ia', 'id', 'ie', 'ig',
'ii', 'ik', 'io', 'is', 'it', 'iu', 'ja', 'jv', 'ka', 'kg', 'ki', 'kj', 'kk', 'kl', 'km', 'kn', 'ko',
'kr', 'ks', 'ku', 'kv', 'kw', 'ky', 'la', 'lb', 'lg', 'li', 'ln', 'lo', 'lt', 'lu', 'lv', 'mg', 'mh',
'mi', 'mk', 'ml', 'mn', 'mo', 'mr', 'ms', 'mt', 'my', 'na', 'nb', 'nd', 'ne', 'ng', 'nl', 'nn', 'no',
'nr', 'nv', 'ny', 'oc', 'oj', 'om', 'or', 'os', 'pa', 'pi', 'pl', 'ps', 'pt', 'qu', 'rm', 'rn', 'ro',
'ru', 'rw', 'sa', 'sc', 'sd', 'se', 'sg', 'si', 'sk', 'sl', 'sm', 'sn', 'so', 'sq', 'sr', 'ss', 'st',
'su', 'sv', 'sw', 'ta', 'te', 'tg', 'th', 'ti', 'tk', 'tl', 'tn', 'to', 'tr', 'ts', 'tt', 'tw', 'ty',
'ug', 'uk', 'ur', 'uz', 've', 'vi', 'vo', 'wa', 'wo', 'xh', 'yi', 'yo', 'za', 'zh', 'zu']) # ISO 639-1
PLUGINS = ['BierDopje', 'OpenSubtitles', 'SubsWiki', 'Subtitulos', 'TheSubDB']
API_PLUGINS = filter(lambda p: getattr(plugins, p).api_based, PLUGINS)
IDLE = 0
RUNNING = 1
PAUSED = 2
class Subliminal(object):
"""Main Subliminal class"""
def __init__(self, cache_dir=None, workers=4, multi=False, force=False, max_depth=3, files_mode=-1):
self.multi = multi
self.force = force
self.max_depth = max_depth
self.cache_dir = None
self.taskQueue = Queue.PriorityQueue()
self.listResultQueue = Queue.Queue()
self.downloadResultQueue = Queue.Queue()
self._languages = []
self._plugins = API_PLUGINS
self.workers = workers
self.files_mode = files_mode
self.state = IDLE
try:
if cache_dir:
self.cache_dir = cache_dir
if not os.path.isdir(self.cache_dir):
os.makedirs(self.cache_dir)
logger.debug(u'Creating cache directory: %r' % self.cache_dir)
except:
self.cache_dir = None
logger.error(u'Failed to use the cache directory, continue without it')
@property
def languages(self):
"""Getter for languages"""
return self._languages
@languages.setter
def languages(self, languages):
"""Setter for languages"""
logger.debug(u'Setting languages to %r' % languages)
self._languages = []
for l in languages:
if l not in LANGUAGES:
raise LanguageError(l)
if not l in self._languages:
self._languages.append(l)
@property
def plugins(self):
"""Getter for plugins"""
return self._plugins
@plugins.setter
def plugins(self, plugins):
"""Setter for plugins"""
logger.debug(u'Setting plugins to %r' % plugins)
self._plugins = []
for p in plugins:
if p not in PLUGINS:
raise PluginError(p)
if not p in self._plugins:
self._plugins.append(p)
def listSubtitles(self, entries, auto=True):
"""
Search subtitles within the plugins and return all found subtitles in a list of Subtitle object.
Attributes:
entries -- filepath or folderpath of video file or a list of that
auto -- automaticaly manage workers"""
if auto:
if self.state != IDLE:
raise BadStateError(self.state, IDLE)
self.startWorkers()
if isinstance(entries, basestring):
entries = [entries]
scan_result = []
for e in entries:
if not isinstance(e, unicode):
logger.warning(u'Entry %r is not unicode' % e)
if not os.path.exists(e):
scan_result.append((e, set(), False))
continue
scan_result.extend(scan(e))
task_count = 0
for filepath, languages, has_single in scan_result:
wanted_languages = set(self._languages)
if not wanted_languages:
wanted_languages = LANGUAGES
if not self.force and self.multi:
wanted_languages = set(wanted_languages) - languages
if not wanted_languages:
logger.debug(u'No need to list multi subtitles %r for %r because %r subtitles detected' % (self._languages, filepath, languages))
continue
if not self.force and not self.multi and has_single:
logger.debug(u'No need to list single subtitles %r for %r because one detected' % (self._languages, filepath))
continue
logger.debug(u'Listing subtitles %r for %r with %r' % (wanted_languages, filepath, self._plugins))
for plugin in self._plugins:
self.taskQueue.put((5, ListTask(filepath, wanted_languages, plugin, self.getConfigDict())))
task_count += 1
subtitles = []
for _ in range(task_count):
subtitles.extend(self.listResultQueue.get())
if auto:
self.stopWorkers()
return subtitles
def downloadSubtitles(self, entries, auto=True):
"""
Download subtitles using the plugins preferences and languages. Also use internal algorithm to find
the best match inside a plugin.
Attributes:
entries -- filepath or folderpath of video file or a list of that
auto -- automaticaly manage workers"""
if auto:
if self.state != IDLE:
raise BadStateError(self.state, IDLE)
self.startWorkers()
subtitles = self.listSubtitles(entries, False)
task_count = 0
for _, subsByVideoPath in groupby(sorted(subtitles, key=lambda x: x.video_path), lambda x: x.video_path):
if not self.multi:
self.taskQueue.put((5, DownloadTask(sorted(list(subsByVideoPath), cmp=self.cmpSubtitles))))
task_count += 1
continue
for __, subsByVideoPathByLanguage in groupby(sorted(subsByVideoPath, key=lambda x: x.language), lambda x: x.language):
self.taskQueue.put((5, DownloadTask(sorted(list(subsByVideoPathByLanguage), cmp=self.cmpSubtitles))))
task_count += 1
downloaded = []
for _ in range(task_count):
downloaded.extend(self.downloadResultQueue.get())
if auto:
self.stopWorkers()
return downloaded
def cmpSubtitles(self, x, y):
"""Compares 2 subtitles elements x and y using video_path, languages and plugin"""
video_paths = sorted([x.video_path, y.video_path])
if x.video_path != y.video_path and video_paths.index(x.video_path) < video_paths(y.video_path):
return - 1
if x.video_path != y.video_path and video_paths.index(x.video_path) > video_paths(y.video_path):
return 1
if self._languages and self._languages.index(x.language) < self._languages.index(y.language):
return - 1
if self._languages and self._languages.index(x.language) > self._languages.index(y.language):
return 1
if self._plugins.index(x.plugin) < self._plugins.index(y.plugin):
return - 1
if self._plugins.index(x.plugin) > self._plugins.index(y.plugin):
return 1
return 0
def startWorkers(self):
"""Create a pool of workers and start them"""
self.pool = []
for _ in range(self.workers):
worker = PluginWorker(self.taskQueue, self.listResultQueue, self.downloadResultQueue)
worker.start()
self.pool.append(worker)
logger.debug(u'Worker %s added to the pool' % worker.name)
self.state = RUNNING
def stopWorkers(self):
"""Stop workers using a lowest priority stop signal and wait for them to terminate properly"""
for _ in range(self.workers):
self.taskQueue.put((10, StopTask()))
for worker in self.pool:
worker.join()
self.state = IDLE
def pauseWorkers(self):
"""Pause workers using a highest priority stop signal and wait for them to terminate properly"""
for _ in range(self.workers):
self.taskQueue.put((0, StopTask()))
for worker in self.pool:
worker.join()
self.state = PAUSED
if self.taskQueue.empty():
self.state = STOPPED
def getConfigDict(self):
"""Produce a dict with configuration items. Used by plugins to read configuration"""
config = {}
config['multi'] = self.multi
config['cache_dir'] = self.cache_dir
config['files_mode'] = self.files_mode
return config
def addTask(self, task):
if not isinstance(task, Task) or isinstance(task, StopTask):
raise WrongTaskError()
self.taskQueue.put((5, task))
class PluginWorker(threading.Thread):
"""Threaded plugin worker"""
def __init__(self, taskQueue, listResultQueue, downloadResultQueue):
threading.Thread.__init__(self)
self.taskQueue = taskQueue
self.listResultQueue = listResultQueue
self.downloadResultQueue = downloadResultQueue
self.logger = logging.getLogger('subliminal.worker')
def run(self):
while True:
task = self.taskQueue.get()[1]
if isinstance(task, StopTask):
self.logger.debug(u'Poison pill received, terminating thread %s' % self.name)
self.taskQueue.task_done()
break
result = []
try:
if isinstance(task, ListTask):
plugin = getattr(plugins, task.plugin)(task.config)
result = plugin.list(task.filepath, task.languages)
elif isinstance(task, DownloadTask):
for subtitle in task.subtitles:
plugin = getattr(plugins, subtitle.plugin)()
try:
result = [plugin.download(subtitle)]
break
except DownloadFailedError as e:
self.logger.warning(u'Could not download subtitle %r, trying next' % subtitle)
continue
if not result:
self.logger.error(u'No subtitles could be downloaded for file %r' % subtitle.video_path)
except:
self.logger.error(u'Exception raised in worker %s' % self.name, exc_info=True)
finally:
if isinstance(task, ListTask):
self.listResultQueue.put(result)
elif isinstance(task, DownloadTask):
self.downloadResultQueue.put(result)
self.taskQueue.task_done()
self.logger.debug(u'Thread %s terminated' % self.name)
def scan(entry, depth=0, max_depth=3):
"""Scan a path and return a list of tuples (filepath, set(languages), has single)"""
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.isfile(entry): # a file? scan it
if depth != 0: # trust the user: only check for valid format if recursing
mimetypes.add_type('video/x-matroska', '.mkv')
if mimetypes.guess_type(entry)[0] not in FORMATS:
return []
# check for .lg.ext and .ext
available_languages = set()
has_single = False
basepath = os.path.splitext(entry)[0]
for l in LANGUAGES:
for e in EXTENSIONS:
if os.path.exists(basepath + '.%s.%s' % (l, e)):
available_languages.add(l)
if os.path.exists(basepath + '.%s' % e):
has_single = True
return [(os.path.normpath(entry), available_languages, has_single)]
if os.path.isdir(entry): # a dir? recurse
result = []
for e in os.listdir(entry):
result.extend(scan(os.path.join(entry, e), depth + 1))
return result
return [] # anything else
+187
View File
@@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import os.path
import babelfish
import pysrt
import re
from .video import Episode, Movie
logger = logging.getLogger(__name__)
#: The following characters are always stripped
IGNORED_CHARACTERS_RE = re.compile('[!@#$\'"]')
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, hi_score_adjust=0):
"""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`
:param hi_score_adjust: adjust hearing impaired matched videos by this value
: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 -= set(['series', 'tvdb_id', 'season', 'episode', 'title'])
if 'tvdb_id' in matches:
matches -= set(['series',])
if 'title' in matches:
matches -= set(['season', 'episode'])
# add other scores
score += sum([video.scores[match] for match in matches])
# Adjust scoring if hearing impaired subtitles are detected
if self.hearing_impaired and hi_score_adjust != 0:
logger.debug('Hearing impaired subtitle score adjusted ' + \
'by %d' % hi_score_adjust)
# Priortization (adjust score)
score += hi_score_adjust
logger.debug('Computed score %d with matches %r', score, initial_matches)
return score
def __repr__(self):
return '<%s [%s]>' % (self.__class__.__name__, self.language)
def sanitize_string(str_in):
"""
Sanitizes a string passed into it by eliminating characters that might
otherwise cause issues when attempting to locate a match on websites by
striping out any special characters and forcing a consistent string that
can be used for caching too.
:param string str_in: the string to sanitize
:return: sanitized string
:rtype: string
"""
if not isinstance(str_in, basestring):
# handle int, float, etc
str_in = str(str_in)
return IGNORED_CHARACTERS_RE.sub('', str_in).lower().strip()
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.LanguageConvertError:
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 as e:
if e.args[0] > 80:
return True
except:
logger.exception('Unexpected error when validating subtitle')
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
+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) # @UndefinedVariable
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'})]
+383
View File
@@ -0,0 +1,383 @@
#!/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.assertEqual(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.assertEqual(show_id, 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)
self.assertEqual(show_ids['the big bang theory'], 126)
def test_query_episode_0(self):
video = EPISODES[0]
languages = set([Language('rus'), Language('heb'), Language('ita'), Language('fra'),
Language('ron'), Language('nld'), Language('eng'), Language('deu'), Language('ell'),
Language('por', 'BR'), Language('bul')])
matches = set([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(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_query_episode_1(self):
video = EPISODES[1]
languages = set([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 = set([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(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_list_subtitles(self):
video = EPISODES[0]
languages = set([Language('eng'), Language('fra')])
matches = set([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(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_download_subtitle(self):
video = EPISODES[0]
languages = set([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 OpenSubtitlesProviderTestCase(ProviderTestCase):
provider_name = 'opensubtitles'
def test_query_movie_0_query(self):
video = MOVIES[0]
languages = set([Language('eng'), ])
matches = set([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(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_query_episode_0_query(self):
video = EPISODES[0]
languages = set([Language('eng'), ])
matches = set([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(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_query_episode_1_query(self):
video = EPISODES[1]
languages = set([Language('eng'), Language('fra')])
matches = set([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(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_query_movie_0_imdb_id(self):
video = MOVIES[0]
languages = set([Language('eng'), Language('fra')])
matches = set([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(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_query_episode_0_imdb_id(self):
video = EPISODES[0]
languages = set([Language('eng'), Language('fra')])
matches = set([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(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_query_movie_0_hash(self):
video = MOVIES[0]
languages = set([Language('eng'), ])
matches = set([frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'imdb_id']),
frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'release_group', 'imdb_id']),
frozenset(['year', 'video_codec', 'imdb_id', 'hash', 'title']),
frozenset([]),
frozenset(['year', 'resolution', 'imdb_id', 'hash', 'title']),
frozenset(['year', 'imdb_id', 'hash', 'title'])])
with self.Provider() as provider:
subtitles = provider.query(languages, hash=video.hashes['opensubtitles'], size=video.size)
self.assertTrue(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_query_episode_0_hash(self):
video = EPISODES[0]
languages = set([Language('eng'), ])
matches = set([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(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_list_subtitles(self):
video = MOVIES[0]
languages = set([Language('eng'), Language('fra')])
matches = set([frozenset(['title', 'video_codec', 'year', 'resolution', 'release_group', 'imdb_id']),
frozenset(['imdb_id', 'year', 'title']),
frozenset(['year', 'video_codec', 'imdb_id', 'resolution', 'title']),
frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'release_group', 'imdb_id']),
frozenset(['year', 'video_codec', 'imdb_id', 'hash', 'title']),
frozenset([]),
frozenset(['year', 'resolution', 'imdb_id', 'hash', 'title']),
frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'imdb_id']),
frozenset(['year', 'imdb_id', 'hash', 'title']),
frozenset(['video_codec', 'imdb_id', 'year', 'title']),
frozenset(['year', 'imdb_id', 'resolution', 'title'])])
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_download_subtitle(self):
video = MOVIES[0]
languages = set([Language('eng'), Language('fra')])
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class PodnapisiProviderTestCase(ProviderTestCase):
provider_name = 'podnapisi'
def test_query_movie_0(self):
video = MOVIES[0]
language = Language('eng')
matches = set([frozenset(['video_codec', 'title', 'resolution', 'year']),
frozenset(['title', 'resolution', 'year']),
frozenset(['video_codec', 'title', 'year']),
frozenset(['title', 'year']),
frozenset(['video_codec', 'title', 'resolution', 'release_group', 'year']),
frozenset(['video_codec', 'title', 'resolution', 'audio_codec', 'year'])])
with self.Provider() as provider:
subtitles = provider.query(language, title=video.title, year=video.year)
self.assertTrue(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue([Language, ], [subtitle.language for subtitle in subtitles])
def test_query_episode_0(self):
video = EPISODES[0]
language = Language('eng')
matches = set([frozenset(['episode', 'series', 'season', 'video_codec', 'resolution', 'release_group']),
frozenset(['season', 'video_codec', 'episode', 'resolution', 'series'])])
with self.Provider() as provider:
subtitles = provider.query(language, series=video.series, season=video.season, episode=video.episode)
self.assertTrue(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue([Language, ], [subtitle.language for subtitle in subtitles])
def test_list_subtitles(self):
video = MOVIES[0]
languages = set([Language('eng'), Language('fra')])
matches = set([frozenset(['video_codec', 'title', 'resolution', 'year']),
frozenset(['title', 'resolution', 'year']),
frozenset(['video_codec', 'title', 'year']),
frozenset(['title', 'year']),
frozenset(['video_codec', 'title', 'resolution', 'release_group', 'year']),
frozenset(['video_codec', 'title', 'resolution', 'audio_codec', 'year'])])
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_download_subtitle(self):
video = MOVIES[0]
languages = set([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 = set([Language('eng'), Language('spa'), Language('por')])
matches = set([frozenset(['hash']), ])
with self.Provider() as provider:
subtitles = provider.query(video.hashes['thesubdb'])
self.assertTrue(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_query_episode_1(self):
video = EPISODES[1]
languages = set([Language('eng'), Language('por')])
matches = set([frozenset(['hash']), ])
with self.Provider() as provider:
subtitles = provider.query(video.hashes['thesubdb'])
self.assertTrue(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_list_subtitles(self):
video = MOVIES[0]
languages = set([Language('eng'), Language('por')])
matches = set([frozenset(['hash']), ])
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
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.assertEqual(show_id, 154)
def test_find_show_id_ambiguous(self):
with self.Provider() as provider:
show_id = provider.find_show_id('New Girl')
self.assertEqual(show_id, 977)
def test_find_show_id_no_dots(self):
with self.Provider() as provider:
show_id = provider.find_show_id('Marvel\'s Agents of S H I E L D')
self.assertEqual(show_id, 1340)
def test_find_show_id_error(self):
with self.Provider() as provider:
show_id = provider.find_show_id('the big gaming')
self.assertEqual(show_id, None)
def test_find_episode_ids(self):
with self.Provider() as provider:
episode_ids = provider.find_episode_ids(154, 5)
self.assertEqual(set(episode_ids.keys()), set(range(1, 25)))
def test_query_episode_0(self):
video = EPISODES[0]
languages = set([Language('fra'), Language('por'), Language('hun'), Language('ron'), Language('eng')])
matches = set([frozenset(['series', 'episode', 'resolution', 'season']),
frozenset(['series', 'episode', 'season'])])
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season, video.episode)
self.assertTrue(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_query_episode_1(self):
video = EPISODES[1]
languages = set([Language('fra'), Language('ell'), Language('ron'), Language('eng'), Language('hun'),
Language('por'), Language('por', 'BR')])
matches = set([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(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
def test_list_subtitles(self):
video = EPISODES[0]
languages = set([Language('eng'), Language('fra')])
matches = set([frozenset(['series', 'episode', 'resolution', 'season']),
frozenset([u'series', u'episode', u'season'])])
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue(matches - set([frozenset(subtitle.compute_matches(video)) for subtitle in subtitles]) == set([]))
self.assertTrue(languages - set([subtitle.language for subtitle in subtitles]) == set([]))
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(OpenSubtitlesProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(PodnapisiProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(TheSubDBProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(TVsubtitlesProviderTestCase))
return suite
if __name__ == '__main__':
TextTestRunner().run(suite())
+175
View File
@@ -0,0 +1,175 @@
#!/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
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 = set([ Language('eng'), ])
subtitles = list_subtitles(videos, languages)
self.assertEqual(len(subtitles), len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_list_subtitles_movie_0_por_br(self):
videos = [MOVIES[0]]
languages = set([Language('por', 'BR'), ])
subtitles = list_subtitles(videos, languages)
self.assertEqual(len(subtitles), len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_list_subtitles_episodes(self):
videos = [EPISODES[0], EPISODES[1]]
languages = set([Language('eng'), Language('fra')])
subtitles = list_subtitles(videos, languages)
self.assertEqual(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 = set([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 = set([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 = set([Language('eng'), Language('fra')])
subtitles = download_best_subtitles(videos, languages)
for video in videos:
self.assertTrue(video in subtitles)
self.assertTrue(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 = set([Language('eng'), Language('fra')])
subtitles = download_best_subtitles(videos, languages, single=True)
for video in videos:
self.assertTrue(video in subtitles)
self.assertEqual(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 = set([Language('eng'), Language('fra')])
subtitles = download_best_subtitles(videos, languages, min_score=1000)
self.assertEqual(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 = set([Language('eng'), ])
subtitles = download_best_subtitles(videos, languages, hearing_impaired=True)
self.assertTrue(subtitles[videos[0]][0].hearing_impaired)
class VideoTestCase(TestCase):
def setUp(self):
os.mkdir(TEST_DIR)
for video in MOVIES + EPISODES:
open(os.path.join(TEST_DIR, os.path.split(video.name)[1]), 'w').close()
def tearDown(self):
shutil.rmtree(TEST_DIR)
def test_scan_video_movie(self):
video = MOVIES[0]
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertEqual(scanned_video.name, os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertEqual(scanned_video.title.lower(), video.title.lower())
self.assertEqual(scanned_video.year, video.year)
self.assertEqual(scanned_video.video_codec, video.video_codec)
self.assertEqual(scanned_video.resolution, video.resolution)
self.assertEqual(scanned_video.release_group, video.release_group)
self.assertEqual(scanned_video.subtitle_languages, set())
self.assertEqual(scanned_video.hashes, {})
self.assertEqual(scanned_video.audio_codec, None)
self.assertEqual(scanned_video.imdb_id, None)
self.assertEqual(scanned_video.size, 0)
def test_scan_video_episode(self):
video = EPISODES[0]
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertEqual(scanned_video.name, os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertEqual(scanned_video.series, video.series)
self.assertEqual(scanned_video.season, video.season)
self.assertEqual(scanned_video.episode, video.episode)
self.assertEqual(scanned_video.video_codec, video.video_codec)
self.assertEqual(scanned_video.resolution, video.resolution)
self.assertEqual(scanned_video.release_group, video.release_group)
self.assertEqual(scanned_video.subtitle_languages, set())
self.assertEqual(scanned_video.hashes, {})
self.assertEqual(scanned_video.title, None)
self.assertEqual(scanned_video.tvdb_id, None)
self.assertEqual(scanned_video.imdb_id, None)
self.assertEqual(scanned_video.audio_codec, None)
self.assertEqual(scanned_video.size, 0)
def test_scan_video_subtitle_language_und(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertEqual(scanned_video.subtitle_languages, set([Language('und'), ]))
def test_scan_video_subtitles_language_eng(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.en.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertEqual(scanned_video.subtitle_languages, set([Language('eng'), ]))
def test_scan_video_subtitles_languages(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.en.srt', 'w').close()
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.fr.srt', 'w').close()
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertEqual(scanned_video.subtitle_languages, set([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())
-22
View File
@@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__version__ = '0.4'
+416
View File
@@ -0,0 +1,416 @@
# -*- 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)
def __eq__(self, other):
return self.__class__.__name__ == other.__class__.__name__\
and self.name == other.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)
def __hash__(self):
return hash((
self.series,
self.season,
self.episode,
))
def __eq__(self, other):
return self.__class__.__name__ == other.__class__.__name__\
and self.series == other.series\
and self.season == other.season\
and self.episode == other.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 __hash__(self):
if self.year is None:
return hash((
self.title,
self.year,
))
return hash(self.title)
def __eq__(self, other):
return self.__class__.__name__ == other.__class__.__name__\
and self.title == other.title\
and self.year == other.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.language_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, video=None):
"""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
:parm :class:`Video`: optionally specify a video if you've already detected on
by other means.
: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)
if not video:
video = Video.fromguess(
path,
guessit.guess_file_info(path, info=['filename']),
)
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:
if st.language:
try:
embedded_subtitle_languages.add(babelfish.Language.fromalpha3b(st.language))
except babelfish.Error:
logger.error('Embedded subtitle track language %r is not a valid language', st.language)
embedded_subtitle_languages.add(babelfish.Language('und'))
elif st.name:
try:
embedded_subtitle_languages.add(babelfish.Language.fromname(st.name))
except babelfish.Error:
logger.error('Embedded subtitle track name %r is not a valid language', st.name)
embedded_subtitle_languages.add(babelfish.Language('und'))
else:
embedded_subtitle_languages.add(babelfish.Language('und'))
logger.debug('Found embedded subtitle %r with enzyme', embedded_subtitle_languages)
video.subtitle_languages |= embedded_subtitle_languages
else:
logger.debug('MKV has no subtitle track')
except enzyme.Error:
logger.error('Parsing video metadata with enzyme failed')
return video
def scan_videos(paths, subtitles=True, embedded_subtitles=True, age=None):
"""Scan `paths` for videos and their subtitle languages
:params paths: absolute paths to scan for videos
: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', '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', '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', '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')
-152
View File
@@ -1,152 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import unittest
import logging
import os
logging.basicConfig(level=logging.DEBUG, format='%(name)-24s %(levelname)-8s %(message)s')
test_folder = u'/your/path/here/videos/'
test_file = u'/your/path/here/videos/the.big.bang.theory.s04e01.hdtv.xvid-fqm.avi'
cache_dir = u'/tmp/sublicache'
if not os.path.exists(cache_dir):
os.mkdir(cache_dir)
class Addic7edTestCase(unittest.TestCase):
def setUp(self):
from subliminal.plugins import Addic7ed
self.config = {'multi': True, 'cache_dir': cache_dir, 'files_mode': -1}
self.plugin = Addic7ed(self.config)
self.languages = set(['en', 'fr'])
def test_list(self):
list = self.plugin.list(test_file, self.languages)
assert list
def test_download(self):
subtitle = self.plugin.list(test_file, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
download = self.plugin.download(subtitle)
assert download
class BierDopjeTestCase(unittest.TestCase):
def setUp(self):
from subliminal.plugins import BierDopje
self.config = {'multi': True, 'cache_dir': cache_dir, 'files_mode': -1}
self.plugin = BierDopje(self.config)
self.languages = set(['en', 'fr'])
def test_list(self):
list = self.plugin.list(test_file, self.languages)
assert list
def test_download(self):
subtitle = self.plugin.list(test_file, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
download = self.plugin.download(subtitle)
assert download
class OpenSubtitlesTestCase(unittest.TestCase):
def setUp(self):
from subliminal.plugins import OpenSubtitles
self.config = {'multi': True, 'cache_dir': cache_dir, 'files_mode': -1}
self.plugin = OpenSubtitles(self.config)
self.languages = set(['en', 'fr'])
def test_list(self):
list = self.plugin.list(test_file, self.languages)
assert list
def test_download(self):
subtitle = self.plugin.list(test_file, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
download = self.plugin.download(subtitle)
assert download
class SubsWikiTestCase(unittest.TestCase):
def setUp(self):
from subliminal.plugins import SubsWiki
self.config = {'multi': True, 'cache_dir': cache_dir, 'files_mode': -1}
self.plugin = SubsWiki(self.config)
self.languages = set(['en', 'fr', 'es', 'pt'])
def test_list(self):
list = self.plugin.list(test_file, self.languages)
assert list
def test_download(self):
subtitle = self.plugin.list(test_file, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
download = self.plugin.download(subtitle)
assert download
class SubtitulosTestCase(unittest.TestCase):
def setUp(self):
from subliminal.plugins import Subtitulos
self.config = {'multi': True, 'cache_dir': cache_dir, 'files_mode': -1}
self.plugin = Subtitulos(self.config)
self.languages = set(['en', 'fr', 'es', 'pt'])
def test_list(self):
list = self.plugin.list(test_file, self.languages)
assert list
def test_download(self):
subtitle = self.plugin.list(test_file, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
download = self.plugin.download(subtitle)
assert download
class TheSubDBTestCase(unittest.TestCase):
def setUp(self):
from subliminal.plugins import TheSubDB
self.config = {'multi': True, 'cache_dir': cache_dir, 'files_mode': -1}
self.plugin = TheSubDB(self.config)
self.languages = set(['en', 'fr', 'es', 'pt'])
def test_list(self):
list = self.plugin.list(test_file, self.languages)
assert list
def test_download(self):
subtitle = self.plugin.list(test_file, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
download = self.plugin.download(subtitle)
assert download
if __name__ == "__main__":
unittest.main()
-90
View File
@@ -1,90 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 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 Lesser GNU 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import unittest
import logging
import os
import subliminal
logging.basicConfig(level=logging.DEBUG, format='%(name)-24s %(levelname)-8s %(message)s')
test_folder = u'/your/path/here/videos/'
test_file = u'/your/path/here/videos/the.big.bang.theory.s04e01.hdtv.xvid-fqm.avi'
cache_dir = u'/tmp/sublicache'
if not os.path.exists(cache_dir):
os.mkdir(cache_dir)
class FileTestCase(unittest.TestCase):
def setUp(self):
self.subli = subliminal.Subliminal(cache_dir=cache_dir, workers=4, multi=False, force=True, max_depth=3, files_mode=-1)
self.subli.languages = ['en', 'fr', 'es', 'pt']
self.subli.plugins = subliminal.PLUGINS
def test_list(self):
results = self.subli.listSubtitles(test_file)
self.assertTrue(len(results) > 0)
def test_download(self):
results = self.subli.downloadSubtitles(test_file)
self.assertTrue(len(results) > 0)
class ErrorTestCase(unittest.TestCase):
def setUp(self):
self.subli = subliminal.Subliminal(cache_dir=cache_dir, workers=4, multi=False, force=True, max_depth=3, files_mode=-1)
def test_language(self):
with self.assertRaises(subliminal.classes.LanguageError):
self.subli.languages = ['en', 'fr', 'zz', 'pt']
def test_plugin(self):
with self.assertRaises(subliminal.classes.PluginError):
self.subli.plugins = ['WrongPlugin']
class PriorityQueueTestCase(unittest.TestCase):
def setUp(self):
self.subli = subliminal.Subliminal(cache_dir=cache_dir, workers=4, multi=False, force=True, max_depth=3, files_mode=-1)
self.subli.languages = ['en', 'fr', 'es', 'pt']
self.subli.plugins = subliminal.PLUGINS
def test_bad_state_error(self):
with self.assertRaises(subliminal.classes.BadStateError):
self.subli.startWorkers()
results = self.subli.listSubtitles(test_folder)
self.subli.stopWorkers()
def test_manual_list(self):
self.subli.taskQueue.put((5, subliminal.classes.ListTask(test_file, set(self.subli.languages), 'OpenSubtitles', self.subli.getConfigDict())))
self.subli.startWorkers()
# parallel stuff...
self.subli.stopWorkers()
result = self.subli.listResultQueue.get()
self.assertTrue(len(result) > 0)
if __name__ == "__main__":
unittest.main()