Compare commits

...

366 Commits

Author SHA1 Message Date
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 3cf265f6c9 Change version from 1.0 to 0.4 2011-11-11 20:37:19 +01:00
Antoine Bertin 1b8dd8cc83 Update gitignore 2011-11-11 20:37:03 +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 f3292c2916 Release v1.0b6 2011-10-08 19:44:14 +02:00
Antoine Bertin 048d6adfa3 Exit the plugin if no correct language is given 2011-10-08 19:41:04 +02:00
Antoine Bertin f004271e8b Fix log message in PluginBase 2011-10-08 19:37:33 +02:00
Antoine Bertin 63ef2f1273 Release v1.0b5 2011-10-08 19:03:51 +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
Antoine Bertin f7f2b80028 Update unittest 2011-10-07 19:22:45 +02:00
Antoine Bertin ec21d564b2 Handle correctly non existent file paths 2011-10-07 19:13:21 +02:00
Antoine Bertin 6d2b772f21 Add a timeout on downloadFile method so it does not get stuck 2011-10-07 19:11:05 +02:00
Antoine Bertin d506dd2095 Remove useless checkLanguages method 2011-10-07 19:10:11 +02:00
Antoine Bertin c66028793d Release v1.0b4 2011-10-07 13:59:37 +02:00
Antoine Bertin 0e3467d3aa Add possible_languages method to filter undesired languages 2011-10-07 01:35:34 +02:00
Antoine Bertin d9dc6832e3 Be more pythonic
- Use property decorators
- Use new-style classes
2011-10-07 01:33:28 +02:00
Antoine Bertin a57d49abf1 Remove useless comments 2011-10-07 01:31:50 +02:00
Antoine Bertin 614388e28f Fix bad error logging when there is no error 2011-10-07 01:31:02 +02:00
Antoine Bertin 7e1a09fb64 Do not forget to remove the gziped file in OpenSubtitles 2011-10-07 01:30:02 +02:00
Antoine Bertin 6efbbd9cad Use logging to print stack instead of traceback 2011-10-07 00:00:07 +02:00
Antoine Bertin e0b5894e3f Use sets in listSubtitles for wanted languages 2011-10-06 23:59:03 +02:00
Antoine Bertin 5ed153425b Preserve order for languages and plugins 2011-10-06 23:56:09 +02:00
Antoine Bertin c961c9be93 Fix multiple issues
- Add a DownloadFailedError exception
- Fix not working "skip on failure" on a download task
- Add extensions to detect subtitles files
- Replace recursiveSearch with scan method
- Fix 'list' object has no attribute 'path'
2011-10-05 22:30:33 +02:00
Antoine Bertin 0bafc9d31f Remove Addic7ed from plugins (not working anymore) 2011-10-05 21:31:54 +02:00
Antoine Bertin a48cd6c824 Remove duplicates in setters for languages and plugins 2011-10-02 21:23:46 +02:00
Antoine Bertin cb899b81e4 Release v1.0b3 2011-10-01 17:02:35 +02:00
Antoine Bertin 918151013e Improve error handling in subliminal 2011-10-01 17:01:54 +02:00
Antoine Bertin 4cb9ebb5e3 Fix search with special characters in some plugins 2011-10-01 15:06:46 +02:00
Antoine Bertin e14a5f1776 Improve error handling in plugins 2011-10-01 15:04:21 +02:00
Antoine Bertin 2437dc602c More logging in plugins 2011-10-01 15:02:38 +02:00
Antoine Bertin 8962d84ed3 Release v1.0b2 2011-09-29 23:55:58 +02:00
Antoine Bertin 4172d2a8b1 Change the Subtitle class. Make download return a list of Subtitles 2011-09-29 23:55:33 +02:00
Antoine Bertin 78a2166efe Release v1.0b 2011-09-29 23:51:30 +02:00
Antoine Bertin 20a2519d92 Fix unittest 2011-09-28 21:54:45 +02:00
Antoine Bertin a7da526885 Compare lowercase only in OpenSubtitles 2011-09-28 21:53:18 +02:00
Antoine Bertin d2010c3510 Log a warning if non unicode is submitted 2011-09-28 21:40:38 +02:00
Antoine Bertin 14c0c6fe6b Fix possible encoding issues in logger using repr instead of str 2011-09-28 21:39:48 +02:00
Antoine Bertin 5d8be66167 Add docstrings in classes 2011-09-28 21:37:37 +02:00
Antoine Bertin 9ac838a257 Make cmpSubtitles and recursiveSearch public 2011-09-27 21:38:30 +02:00
Antoine Bertin 0cd1016cae Use two result queues instead of one 2011-09-27 21:35:14 +02:00
Antoine Bertin 49b8486f44 Use simple quotes 2011-09-27 21:26:27 +02:00
Antoine Bertin 0a363ba5d5 Add a method to add tasks with default priority 2011-09-27 21:24:55 +02:00
Antoine Bertin 5fbfaef31d Fix BadStateError in CLI 2011-09-26 21:41:44 +02:00
Antoine Bertin 9324fc4c07 Add auto argument to manage workers 2011-09-26 21:41:20 +02:00
Antoine Bertin 171147dfcd Fix bad argument in unittest 2011-09-26 21:39:04 +02:00
Antoine Bertin dee0b33cec Fix missing import 2011-09-26 21:37:52 +02:00
Antoine Bertin f9c7654b7c Update unittest 2011-09-24 16:51:37 +02:00
Antoine Bertin b1e0371bb5 Use a PriorityQueue for tasks 2011-09-24 16:50:55 +02:00
Antoine Bertin f94957c68d Accept basestring input, not just unicode 2011-09-24 16:48:16 +02:00
Antoine Bertin 03dd15f718 Merge pull request #15 from abenea/master
Bugfixes
2011-09-23 15:08:44 -07:00
Andrei Benea 938b30604b Fix --cache_dir. 2011-09-23 21:25:28 +03:00
Andrei Benea acff303897 Fix "Downloaded subtitles" report. 2011-09-23 20:46:37 +03:00
Andrei Benea 978f4fb376 Fix exception in _recursiveSearch if no languages are selected. 2011-09-23 20:39:50 +03:00
Andrei Benea 50e0a6704b Add missing newline. 2011-09-23 20:39:08 +03:00
Andrei Benea b4e9c0e655 Don't hang on exceptions from the main thread. 2011-09-23 20:11:24 +03:00
Antoine Bertin 97ab555e2d Update unittest 2011-09-01 21:35:24 +02:00
Antoine Bertin 9bc12ec619 Fix BierDopje download 2011-09-01 21:35:04 +02:00
Antoine Bertin e4c12dfe5c Remove unused imports and variables 2011-09-01 08:38:53 +02:00
Antoine Bertin ca0d8e4ab1 Update .gitignore 2011-09-01 08:37:54 +02:00
Antoine Bertin 570dc7ef9a Remove last encodingKludge import 2011-08-31 21:38:04 +02:00
Antoine Bertin 3e7cac972d Update unittests 2011-08-29 22:23:24 +02:00
Antoine Bertin 9c7ac431b5 Update docstrings. Double quotes to simple quotes. Minor fixes
Minor fixes includes:
- getClassName() converted to self.__class__.__name__
- Typos
- listTeams separators in plugins to include more separators
2011-08-29 21:36:49 +02:00
Antoine Bertin 8f50b317cc Remove encodingKludge 2011-08-29 21:28:35 +02:00
Antoine Bertin c5e2e8c4b0 Fix wrong subliminal version in TheSubDB 2011-08-29 21:22:58 +02:00
Antoine Bertin 7aec1f7653 Remove unused multi_languages_queries in plugins 2011-08-29 21:22:30 +02:00
Antoine Bertin 65a677e6d4 Remove unused plugins_config 2011-08-29 21:20:40 +02:00
Antoine Bertin 8e9781c081 Add Subtitle and Task classes. Change list and download in plugins 2011-08-29 21:20:00 +02:00
Antoine Bertin d76d81eda3 Fix release group comparison. Use lowercase 2011-08-29 20:50:34 +02:00
Antoine Bertin 7b439bbf6f Remove useless multi_filename_queries stuff 2011-08-28 09:50:01 +02:00
Antoine Bertin aea078d65f Add some features. Review module organization
- More constants (FORMATS, LANGUAGES, PLUGINS, API_PLUGINS)
- NullHandler is given to the logger
- Remove useless stuff
- Custom exceptions
- More strict setters
- Move PluginWorker class to subliminal
2011-08-27 23:44:03 +02:00
Antoine Bertin 5834c3edcf Improve argument naming in subliminal script 2011-08-27 23:29:15 +02:00
Antoine Bertin 1d40c026f4 Remove the configuration file 2011-08-27 22:55:11 +02:00
Antoine Bertin 00fb74c5b1 Replace deprecated optparse with argparse 2011-08-27 18:49:53 +02:00
Antoine Bertin 066dc77bd9 Revert to development version 2011-08-27 18:47:16 +02:00
64 changed files with 6109 additions and 2544 deletions
+37 -3
View File
@@ -1,4 +1,38 @@
build
*.py[co]
# Packages
*.egg
*.egg-info
dist
subliminal.egg-info
*.pyc
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
#Sphinx
docs/_build
+3
View File
@@ -0,0 +1,3 @@
[submodule "docs/_themes"]
path = docs/_themes
url = git://github.com/Diaoul/diaoul-sphinx-themes.git
+9
View File
@@ -0,0 +1,9 @@
language: python
python:
- "2.7"
install:
- pip install -r requirements.txt --use-mirrors
- pip install -r optional-requirements.txt --use-mirrors
script: python setup.py test
notifications:
irc: "irc.freenode.org#subliminal"
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+165
View File
@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
+1
View File
@@ -0,0 +1 @@
include COPYING COPYING.LESSER NEWS.rst README.rst
+69
View File
@@ -0,0 +1,69 @@
News
====
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
-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/)
+49
View File
@@ -0,0 +1,49 @@
Subliminal
==========
.. image:: https://secure.travis-ci.org/Diaoul/subliminal.png?branch=develop
Subliminal is a python library to search and download subtitles.
It uses video hashes and the powerful `guessit <http://guessit.readthedocs.org/>`_ library
that extracts informations from filenames or filepaths to ensure you have the best subtitles.
It also relies on `enzyme <https://github.com/Diaoul/enzyme>`_ to detect embedded subtitles
and avoid duplicates.
Features
--------
Multiple subtitles services are available:
* Addic7ed
* BierDopje
* OpenSubtitles
* Podnapisi
* SubsWiki
* Subtitulos
* TheSubDB
* TvSubtitles
You can use main subliminal's functions with a **file path**, a **file name** or a **folder path**.
CLI
^^^
Download english subtitles::
$ subliminal -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
**************************************************
Downloaded 1 subtitle(s) for 1 video(s)
The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.srt from opensubtitles
**************************************************
Module
^^^^^^
List english subtitles::
>>> subliminal.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
Multi-threaded use
^^^^^^^^^^^^^^^^^^
Use 4 workers to achieve the same result::
>>> with subliminal.Pool(4) as p:
... p.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
+153
View File
@@ -0,0 +1,153 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# 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 " 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 " 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."
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."
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+4
View File
@@ -0,0 +1,4 @@
<h3>About</h3>
<p>
Subliminal is a python library to search and download subtitles.
</p>
+6
View File
@@ -0,0 +1,6 @@
<h3>Useful Links</h3>
<ul>
<li><a href="http://pypi.python.org/pypi/subliminal">subliminal @ PyPI</a></li>
<li><a href="http://github.com/Diaoul/subliminal">subliminal @ GitHub</a></li>
<li><a href="http://github.com/Diaoul/subliminal/issues">Issue Tracker</a></li>
</ul>
Submodule
+1
Submodule docs/_themes added at 4b7a0c3e43
+255
View File
@@ -0,0 +1,255 @@
# -*- coding: utf-8 -*-
#
# subliminal documentation build configuration file, created by
# sphinx-quickstart on Tue Feb 28 16:33:06 2012.
#
# 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.infos
# -- 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']
# 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 = u'subliminal'
copyright = u'2012, Antoine Bertin'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = subliminal.infos.__version__
# The full version, including alpha/beta/rc tags.
release = 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 = []
# -- 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': 'develop',
'fork_me': 1,
'flattr_href': 'http://subliminal.readthedocs.org/',
'flattr_thing_url': 'http://flattr.com/thing/629842/Subliminal'}
# 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 = '_static/subliminal-logo.png'
# 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-watch.html', 'sidebar-travis-ci.html', 'sidebar-donate.html', 'localtoc.html', 'sidebar-links.html', 'searchbox.html'],
'**': ['localtoc.html', 'relations.html', 'sourcelink.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'
# -- Options for autodoc -------------------------------------------------------
autodoc_member_order = 'bysource'
+98
View File
@@ -0,0 +1,98 @@
This guide is going to explain the main logic of subliminal and detail
every class or function.
Services
--------
Subliminal aims at downloading subtitles. Over the web, one can find subtitles
combining different websites but there is no guarantee of a perfect match.
Even if OpenSubtitles has a gigantic subtitles database, you may not be able to
find a subtitle on it but you will find it elsewhere, say BierDopje. Sometimes,
it just takes some time before it shows up on a website even if already available
on another, but you do not want to wait to watch the latest Big Bang Theory, right?
Given this, to be reliable, subliminal has to use different :mod:`~subliminal.services`
and use a unified method to gather them all. The :class:`~subliminal.services.ServiceBase`
class will achieve this.
.. automodule:: subliminal.services
:members:
Languages
---------
To be able to support many languages, subliminal makes heavy use of ISO-3166 and ISO-639
in a dedicated module.
.. automodule:: subliminal.language
:members:
.. data:: subliminal.language.COUNTRIES
ISO-3166-1 countries list from `Debian package iso-codes 3.36-1 <http://packages.debian.org/fr/sid/iso-codes>`_.
Each item of this list is a tuple like ``(alpha2, alpha3, numeric, name)``
.. data:: subliminal.language.LANGUAGES
ISO-639-2 languages list from `the official source <http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt>`_.
Each item of this list is a tuple like ``(alpha3, terminologic, alpha2, name, french_name)``
Tasks
-----
Subliminal is IO bound: it mostly waits for IO operations (web requests) to complete.
Thus, subliminal is a good place for multi-threading. It works with atomic operations
represented by a :class:`~subliminal.tasks.Task` class which can be consumed with
:func:`~subliminal.core.consume_task` but we'll see that later.
.. automodule:: subliminal.tasks
:members:
Asynchronous
------------
To consume those tasks in an asynchronous way without flooding services with requests,
subliminal uses multiple instances of the :class:`~subliminal.async.Worker` class that
will consume the same task queue. Each worker will only create a single instance of each
:mod:`service <subliminal.services>` and this save some initialization time.
The :class:`~subliminal.async.Pool` is here to instantiate and manage multiple workers
at a time.
.. automodule:: subliminal.async
:members:
Core
----
The goal of subliminal's :mod:`~subliminal.core` module is to merge results from
consumed tasks. Merging has to be intelligent and take user preferences into account.
Core module is thus responsible for the computation of a :func:`matching confidence
<subliminal.core.matching_confidence>` so the user knows the chances that the
:class:`~subliminal.subtitles.ResultSubtitle` matches the :class:`~subliminal.videos.Video`
.. automodule:: subliminal.core
:members:
Other objects
-------------
Subliminal uses some other self-explanatory functions and classes listed below.
Video
^^^^^
.. automodule:: subliminal.videos
:members:
Subtitle
^^^^^^^^
.. automodule:: subliminal.subtitles
:members:
Utilities
^^^^^^^^^
.. automodule:: subliminal.utils
:members:
Exceptions
^^^^^^^^^^
.. automodule:: subliminal.exceptions
:members:
+80
View File
@@ -0,0 +1,80 @@
.. subliminal documentation master file, created by
sphinx-quickstart on Tue Feb 28 16:33:06 2012.
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 uses video hashes and the powerful `guessit <http://guessit.readthedocs.org/>`_ library
that extracts informations from filenames or filepaths to ensure you have the best subtitles.
It also relies on `enzyme <https://github.com/Diaoul/enzyme>`_ to detect embedded subtitles
and avoid duplicates.
Features
--------
Multiple subtitles services are available:
* Addic7ed
* BierDopje
* OpenSubtitles
* Podnapisi
* SubsWiki
* Subtitulos
* TheSubDB
* TvSubtitles
You can use main subliminal's functions with a **file path**, a **file name** or a **folder path**.
CLI
^^^
Download english subtitles::
$ subliminal -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
**************************************************
Downloaded 1 subtitle(s) for 1 video(s)
The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.srt from opensubtitles
**************************************************
Module
^^^^^^
List english subtitles::
>>> subliminal.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
Multi-threaded use
^^^^^^^^^^^^^^^^^^
Use 4 workers to achieve the same result::
>>> with subliminal.Pool(4) as p:
... p.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
User Guide
----------
This part of the documentation details how to use subliminal for most common tasks
.. toctree::
:maxdepth: 2
user
Developer Guide
---------------
This part of the documentation explains internal behavior of subliminal and its algorithms
.. toctree::
:maxdepth: 2
dev
API Documentation
-----------------
Most common subliminal features are listed here
.. automodule:: subliminal
:members:
:noindex:
+118
View File
@@ -0,0 +1,118 @@
There are 4 different ways of using subliminal and each one is described in a dedicated section below.
First, here are some basics
Basics
------
Services
^^^^^^^^
You can use subliminal with multiple services to get the best result.
Current available services are available in the :data:`subliminal.SERVICES` variable.
.. autodata:: subliminal.SERVICES
Languages
^^^^^^^^^
Subliminal supports multiple languages representations based on `ISO-639 <http://en.wikipedia.org/wiki/ISO_639>`_
and `ISO-3166 <http://en.wikipedia.org/wiki/ISO_3166>`_. Any single ISO-639 string or combination of ISO-639 and
ISO-3166 is acceptable. For example, you can use ``pt-br`` for Portuguese (Brazil) or ``en`` for English.
Paths
^^^^^
All paths parameters in subliminal most commont functions can be either *a file path*,
*a file name* or a *folder path*
* File path (existing): hashes of the file will be computed and used during the search for services that
supports this functionnality.
* File name (or non-existing file path): the guessit python library will be used to guess informations
and a text-based search will be done with services.
* Folder path (containing video files): the given folder will be searched for video files using their
:data:`~subliminal.videos.MIMETYPES` and/or :data:`~subliminal.videos.EXTENSIONS`.
The default maximum depth to scan is 3
CLI
---
Subliminal is shipped with a Command Line Interface that allows you to
download subtitles for one or more videos in a multithreaded way.
.. note::
The cache directory defaults to *~/.config/subliminal*. Even on Windows
Usage
^^^^^
You can have the documentation of the CLI using ``subliminal --help``::
usage: subliminal [-h] [-l LG] [-s NAME] [-m] [-f] [-w N] [-a AGE] [-c]
[-q | -v] [--cache-dir DIR | --no-cache-dir] [--version]
PATH [PATH ...]
Subtitles, faster than your thoughts
positional arguments:
PATH path to video file or folder
optional arguments:
-h, --help show this help message and exit
-l LG, --language LG wanted language (ISO 639-1)
-s NAME, --service NAME
service to use
-m, --multi download multiple subtitle languages
-f, --force replace existing subtitle file
-w N, --workers N use N threads (default: 4)
-a AGE, --age AGE scan only for files newer or older (prefix with +)
than AGE (e.g. 12h, 1w2d, +3d6h)
-c, --compatibility try not to use unicode (use this if you have encoding
errors)
-q, --quiet disable output
-v, --verbose verbose output
--cache-dir DIR cache directory to use
--no-cache-dir do not use cache directory (some services may not
work)
--version show program's version number and exit
Cron job
^^^^^^^^
This CLI is well suited for automatic subtitles downloads. For example, to download english and french
subtitles for videos newer than one week under /path/to/videos/ each day at 1:00AM with a single worker,
you can use the following crontab line::
0 1 * * * user /path/to/subliminal -m -l en -l fr -w 1 -a 1w -q /path/to/videos/
Simple module use
-----------------
Subliminal comes with two basic functions to search and download subtitles. For example, you
can do::
>>> subliminal.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
.. autofunction:: subliminal.list_subtitles
Or even download missing subtitles for each episodes under the given folders in two different languages::
>>> subliminal.download_subtitles(['/mnt/videos/BBT/Season 05', '/mnt/videos/HIMYM/Season 07'],
... ['en', 'fr'], force=False, multi=True)
.. autofunction:: subliminal.download_subtitles
Multi-threaded module use
-------------------------
You can call the same functions on a :class:`subliminal.Pool` object previously
created with the appropriate number of workers.
.. autoclass:: subliminal.Pool
:members:
You have to call the :meth:`~subliminal.Pool.start` method before any actions and
:meth:`~subliminal.Pool.stop` before exiting your program::
>>> p = subliminal.Pool(4)
... p.start()
... p.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
... p.stop()
To make the use of :class:`~subliminal.Pool` easier, you can use the ``with`` statement
that takes care of that for you::
>>> with subliminal.Pool(4) as p:
... p.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
+1
View File
@@ -0,0 +1 @@
lxml
+5
View File
@@ -0,0 +1,5 @@
beautifulsoup4>=4.0
guessit>=0.4.1
requests
enzyme>=0.1
html5lib
Executable → Regular
+81 -82
View File
@@ -1,100 +1,99 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# 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.
#
# 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
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# 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.
# GNU Lesser 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 optparse import OptionParser
import subliminal
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
import argparse
import datetime
import logging
import mimetypes
import os
import re
import subliminal
import sys
def main():
'''Download subtitles'''
# parse command line options
parser = OptionParser("usage: %prog [options] file1 file2", version=subliminal.__version__)
parser.add_option("-l", "--language", action="append", dest="languages", help="wanted language (ISO 639-1 two chars) for the subtitles (e.g. fr, en). Multiple uses allowed such that `%prog -l fr -l en file1`")
parser.add_option("-m", "--multi", action="store_true", dest="multi", help="download one subtitle per specified language (instead of one of them) and name them accordingly (e.g. .fr.srt, .en.srt)")
parser.add_option("-p", "--plugin", action="append", dest="plugins", help="plugins to activate")
parser.add_option("-f", "--force", action="store_true", dest="force", help="force download of a subtitle even there is already one present")
parser.add_option("-C", "--no-config-file", action="store_false", dest="config", help="do not use configuration file (requires -l to be specified)")
parser.add_option("-c", "--config-file", action="store", dest="config", help="configuration file to use")
parser.add_option("-w", "--workers", action="store", dest="workers", help="specify the number of threads to use")
parser.add_option("--cache-dir", action="store", dest="cache_dir", help="cache directory to use")
parser.add_option("--no-cache-dir", action="store_false", dest="cache_dir", help="do not use cache directory (some plugins may not work)")
parser.add_option("--list-all-plugins", action="store_true", dest="list_all_plugins", help="list all plugins available")
parser.add_option("--list-api-plugins", action="store_true", dest="list_api_plugins", help="list api-based plugins")
parser.add_option("--list-active-plugins", action="store_true", dest="list_active_plugins", help="list currently active plugins")
parser.add_option("-v", "--verbose", action="count", dest="verbose", help="increase verbosity (maximum 2 times)")
parser.set_defaults(verbose=0, cache_dir=True, config=True, workers=4)
(options, args) = parser.parse_args()
if not args:
print parser.print_help()
exit(1)
if options.verbose == 2:
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)-24s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
elif options.verbose == 1:
logging.basicConfig(level=logging.WARN, format='%(levelname)s: %(name)s %(message)s')
else:
logging.basicConfig(level=logging.ERROR)
if not options.config and not options.languages:
parser.error("Option -C (--no-config-file) is used without -l (--language)")
subliminal_client = subliminal.Subliminal(config=options.config, cache_dir=options.cache_dir, workers=options.workers, multi=options.multi, force=options.force, max_depth=3, autostart=False)
if options.plugins:
subliminal_client.plugins = options.plugins
if options.list_all_plugins:
plugins = subliminal_client.listExistingPlugins()
print ', '.join(subliminal_client.listExistingPlugins())
exit(0)
if options.list_api_plugins:
plugins = subliminal_client.list_api_plugins()
print ', '.join(subliminal_client.listExistingPlugins())
exit(0)
if options.list_active_plugins:
plugins = subliminal_client.plugins
print ', '.join(subliminal_client.listExistingPlugins())
exit(0)
if options.languages:
subliminal_client.languages = options.languages
else:
logging.info(u"No language given, looking into configuration file")
languages = subliminal_client.languages
if not languages:
logging.error(u"No language found in configuration file")
sys.stderr.write("No language found in configuration file")
exit(1)
parser.exit
subliminal_client.startWorkers()
subtitles = subliminal_client.downloadSubtitles(args)
subliminal_client.stopWorkers()
if len(subtitles) == 0:
sys.stderr.write("No subtitles found")
exit(1)
print "*" * 50
print "Downloaded %s subtitles" % len(subtitles)
for s in subtitles:
print s['lang'] + " - " + s['subtitlepath']
print "*" * 50
parser = argparse.ArgumentParser(description='Subtitles, faster than your thoughts')
parser.add_argument('-l', '--language', action='append', dest='languages', help='wanted language (ISO 639-1)', metavar='LG')
parser.add_argument('-s', '--service', action='append', dest='services', help='service to use', metavar='NAME')
parser.add_argument('-m', '--multi', action='store_true', help='download multiple subtitle languages')
parser.add_argument('-f', '--force', action='store_true', help='replace existing subtitle file')
parser.add_argument('-w', '--workers', action='store', help='use N threads (default: %(default)s)', metavar='N', type=int, default=4)
parser.add_argument('-a', '--age', action='store', help='scan only for files newer or older (prefix with +) than AGE (e.g. 12h, 1w2d, +3d6h)', metavar='AGE', default=None)
parser.add_argument('-c', '--compatibility', action='store_true', help='try not to use unicode (use this if you have encoding errors)')
group_verbosity = parser.add_mutually_exclusive_group()
group_verbosity.add_argument('-q', '--quiet', action='store_true', help='disable output')
group_verbosity.add_argument('-v', '--verbose', action='store_true', help='verbose output')
group_cache = parser.add_mutually_exclusive_group()
group_cache.add_argument('--cache-dir', action='store', dest='cache_dir', help='cache directory to use', metavar='DIR', default=os.path.expanduser('~/.config/subliminal'))
group_cache.add_argument('--no-cache-dir', action='store_false', dest='cache_dir', help='do not use cache directory (some services may not work)')
parser.add_argument('--version', action='version', version=subliminal.__version__)
parser.add_argument('paths', nargs='+', help='path to video file or folder', metavar='PATH')
args = parser.parse_args()
if __name__ == "__main__":
# Set log verbosity
if args.verbose:
logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(asctime)s %(name)-24s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
elif not args.quiet:
logging.basicConfig(level=logging.WARN, format='%(levelname)s: %(name)s %(message)s')
# Create cache directory
if not os.path.exists(args.cache_dir):
os.mkdir(args.cache_dir)
# Create filter function
scan_filter = None
if args.age:
regex = re.compile(r'^(?P<sign>\+?)((?P<weeks>\d+?)w)?((?P<days>\d+?)d)?((?P<hours>\d+?)h)?')
parts = regex.match(args.age)
if not parts:
raise ValueError('Incorrect age format')
time_params = {}
parts = parts.groupdict()
for name, param in parts.iteritems():
if param and name != 'sign':
time_params[name] = int(param)
if parts['sign'] == '+':
scan_filter = lambda x: datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(x)) < datetime.timedelta(**time_params)
else:
scan_filter = lambda x: datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(x)) > datetime.timedelta(**time_params)
# Compatibility mode
if args.compatibility:
paths = args.paths
else:
paths = [unicode(x) for x in args.paths]
# Download subtitles
with subliminal.Pool(args.workers) as p:
results = p.download_subtitles(paths, languages=args.languages, services=args.services, cache_dir=args.cache_dir,
force=args.force, multi=args.multi, scan_filter=scan_filter)
if not results:
if not args.quiet:
sys.stderr.write('No subtitles downloaded\n')
exit(1)
if not args.quiet:
print '*' * 50
print 'Downloaded %d subtitle(s) for %d video(s)' % (sum([len(s) for s in results.itervalues()]), len(results))
for _, subtitles in results.iteritems():
for subtitle in subtitles:
print '%s from %s' % (subtitle.path, subtitle.service)
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
+27 -19
View File
@@ -1,45 +1,53 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
# This file is part of subliminal.
#
# 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
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# 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.
# GNU Lesser 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
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
import os.path
import sys
from setuptools import setup, find_packages
execfile('subliminal/version.py')
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
required = ['beautifulsoup4 >= 4.0', 'guessit >= 0.4.1', 'requests', 'enzyme >= 0.1', 'html5lib']
if sys.hexversion < 0x20700f0:
required.append('argparse >= 1.1')
execfile(os.path.join(os.path.dirname(__file__), 'subliminal', 'infos.py'))
setup(name='subliminal',
version=__version__,
license='LGPLv3',
description='Subliminal - Subtitles, faster than your thoughts',
description='Subtitles, faster than your thoughts',
long_description=read('README.rst') + '\n\n' + read('NEWS.rst'),
classifiers=['Development Status :: 4 - Beta',
'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
'Intended Audience :: Developers',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Multimedia :: Video'],
keywords='subliminal video movie subtitle python library',
keywords='subtitle subtitles video movie episode tv show',
author='Antoine Bertin',
author_email='diaoulael@gmail.com',
url='https://github.com/Diaoul/subliminal',
packages=['subliminal', 'subliminal/plugins'],
packages=find_packages(),
scripts=['scripts/subliminal'],
py_modules=['subliminal'],
install_requires=['BeautifulSoup>=3.2.0', 'guessit>=0.2'])
test_suite='tests.suite',
install_requires=required,
extras_require={'full': ['lxml']})
-71
View File
@@ -1,71 +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
import plugins
import logging
import traceback
class PluginWorker(threading.Thread):
"""Threaded plugin worker"""
def __init__(self, taskQueue, resultQueue):
threading.Thread.__init__(self)
self.taskQueue = taskQueue
self.resultQueue = resultQueue
self.logger = logging.getLogger('subliminal.worker')
def run(self):
while True:
task = self.taskQueue.get()
result = None
try:
if not task: # this is a poison pill
break
elif task['task'] == 'list': # the task is a listing
# get the corresponding plugin
plugin = getattr(plugins, task['plugin'])(task['config'])
# split tasks if the plugin can't handle multi queries
splitedTasks = plugin.splitTask(task)
myTask = splitedTasks.pop()
for st in splitedTasks:
self.taskQueue.put(st)
result = plugin.list(myTask['filenames'], myTask['languages'])
elif task['task'] == 'download': # the task is to download
result = None
while task['subtitle']:
subtitle = task['subtitle'].pop(0)
# get the corresponding plugin
plugin = getattr(plugins, subtitle['plugin'])(task['config'])
path = plugin.download(subtitle)
if path:
subtitle['subtitlepath'] = path
result = subtitle
break
else:
self.logger.error(u'Unknown task %s submited to worker %s' % (task['task'], self.name))
except:
self.logger.debug(traceback.print_exc())
self.logger.error(u"Worker couldn't do the job %s, continue anyway" % task['task'])
finally:
self.resultQueue.put(result)
self.taskQueue.task_done()
self.logger.debug(u'Thread %s terminated' % self.name)
+24 -13
View File
@@ -1,23 +1,34 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
# This file is part of subliminal.
#
# 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
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# 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/>.
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from .api import list_subtitles, download_subtitles
from .async import Pool
from .core import (SERVICES, LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE,
MATCHING_CONFIDENCE)
from .infos import __version__
import logging
try:
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record):
pass
from subliminal import Subliminal
from version import __version__
__all__ = ['SERVICES', 'LANGUAGE_INDEX', 'SERVICE_INDEX', 'SERVICE_CONFIDENCE',
'MATCHING_CONFIDENCE', 'list_subtitles', 'download_subtitles', 'Pool']
logging.getLogger(__name__).addHandler(NullHandler())
+104
View File
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from .core import (SERVICES, LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE,
MATCHING_CONFIDENCE, create_list_tasks, consume_task, create_download_tasks,
group_by_video, key_subtitles)
from .language import language_set, language_list, LANGUAGES
import logging
__all__ = ['list_subtitles', 'download_subtitles']
logger = logging.getLogger(__name__)
def list_subtitles(paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None):
"""List subtitles in given paths according to the criteria
:param paths: path(s) to video file or folder
:type paths: string or list
:param languages: languages to search for, in preferred order
:type languages: list of :class:`~subliminal.language.Language` or string
:param list services: services to use for the search, in preferred order
:param bool force: force searching for subtitles even if some are detected
:param bool multi: search multiple languages for the same video
:param string cache_dir: path to the cache directory to use
:param int max_depth: maximum depth for scanning entries
:param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
:return: found subtitles
:rtype: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.ResultSubtitle`]
"""
services = services or SERVICES
languages = language_set(languages) if languages is not None else language_set(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
if any([not isinstance(p, unicode) for p in paths]):
logger.warning(u'Not all entries are unicode')
results = []
service_instances = {}
tasks = create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for task in tasks:
try:
result = consume_task(task, service_instances)
results.append((task.video, result))
except:
logger.error(u'Error consuming task %r' % task, exc_info=True)
for service_instance in service_instances.itervalues():
service_instance.terminate()
return group_by_video(results)
def download_subtitles(paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None, order=None):
"""Download subtitles in given paths according to the criteria
:param paths: path(s) to video file or folder
:type paths: string or list
:param languages: languages to search for, in preferred order
:type languages: list of :class:`~subliminal.language.Language` or string
:param list services: services to use for the search, in preferred order
:param bool force: force searching for subtitles even if some are detected
:param bool multi: search multiple languages for the same video
:param string cache_dir: path to the cache directory to use
:param int max_depth: maximum depth for scanning entries
:param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
:param order: preferred order for subtitles sorting
:type list: list of :data:`~subliminal.core.LANGUAGE_INDEX`, :data:`~subliminal.core.SERVICE_INDEX`, :data:`~subliminal.core.SERVICE_CONFIDENCE`, :data:`~subliminal.core.MATCHING_CONFIDENCE`
:return: downloaded subtitles
:rtype: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.ResultSubtitle`]
"""
services = services or SERVICES
languages = language_list(languages) if languages is not None else language_list(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
order = order or [LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE]
subtitles_by_video = list_subtitles(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for video, subtitles in subtitles_by_video.iteritems():
subtitles.sort(key=lambda s: key_subtitles(s, video, languages, services, order), reverse=True)
results = []
service_instances = {}
tasks = create_download_tasks(subtitles_by_video, multi)
for task in tasks:
try:
result = consume_task(task, service_instances)
results.append((task.video, result))
except:
logger.error(u'Error consuming task %r' % task, exc_info=True)
for service_instance in service_instances.itervalues():
service_instance.terminate()
return group_by_video(results)
+142
View File
@@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from .core import (consume_task, LANGUAGE_INDEX, SERVICE_INDEX,
SERVICE_CONFIDENCE, MATCHING_CONFIDENCE, SERVICES, create_list_tasks,
create_download_tasks, group_by_video, key_subtitles)
from .language import language_list, language_set, LANGUAGES
from .tasks import StopTask
import Queue
import logging
import threading
__all__ = ['Worker', 'Pool']
logger = logging.getLogger(__name__)
class Worker(threading.Thread):
"""Consume tasks and put the result in the queue"""
def __init__(self, tasks, results):
super(Worker, self).__init__()
self.tasks = tasks
self.results = results
self.services = {}
def run(self):
while 1:
result = []
try:
task = self.tasks.get(block=True)
if isinstance(task, StopTask):
break
result = consume_task(task, self.services)
self.results.put((task.video, result))
except:
logger.error(u'Exception raised in worker %s' % self.name, exc_info=True)
finally:
self.tasks.task_done()
self.terminate()
logger.debug(u'Thread %s terminated' % self.name)
def terminate(self):
"""Terminate instantiated services"""
for service_name, service in self.services.iteritems():
try:
service.terminate()
except:
logger.error(u'Exception raised when terminating service %s' % service_name, exc_info=True)
class Pool(object):
"""Pool of workers"""
def __init__(self, size):
self.tasks = Queue.Queue()
self.results = Queue.Queue()
self.workers = []
for _ in range(size):
self.workers.append(Worker(self.tasks, self.results))
def __enter__(self):
self.start()
return self
def __exit__(self, *args):
self.stop()
self.join()
def start(self):
"""Start workers"""
for worker in self.workers:
worker.start()
def stop(self):
"""Stop workers"""
for _ in self.workers:
self.tasks.put(StopTask())
def join(self):
"""Join the task queue"""
self.tasks.join()
def collect(self):
"""Collect available results
:return: results of tasks
:rtype: list of :class:`~subliminal.tasks.Task`
"""
results = []
while 1:
try:
result = self.results.get(block=False)
results.append(result)
except Queue.Empty:
break
return results
def list_subtitles(self, paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None):
"""See :meth:`subliminal.list_subtitles`"""
services = services or SERVICES
languages = language_set(languages) if languages is not None else language_set(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
if any([not isinstance(p, unicode) for p in paths]):
logger.warning(u'Not all entries are unicode')
tasks = create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for task in tasks:
self.tasks.put(task)
self.join()
results = self.collect()
return group_by_video(results)
def download_subtitles(self, paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None, order=None):
"""See :meth:`subliminal.download_subtitles`"""
services = services or SERVICES
languages = language_list(languages) if languages is not None else language_list(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
order = order or [LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE]
subtitles_by_video = self.list_subtitles(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for video, subtitles in subtitles_by_video.iteritems():
subtitles.sort(key=lambda s: key_subtitles(s, video, languages, services, order), reverse=True)
tasks = create_download_tasks(subtitles_by_video, multi)
for task in tasks:
self.tasks.put(task)
self.join()
results = self.collect()
return group_by_video(results)
+134
View File
@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
# Copyright 2012 Nicolas Wack <wackou@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from collections import defaultdict
from functools import wraps
import logging
import os.path
import threading
try:
import cPickle as pickle
except ImportError:
import pickle
__all__ = ['Cache', 'cachedmethod']
logger = logging.getLogger(__name__)
class Cache(object):
"""A Cache object contains cached values for methods. It can have
separate internal caches, one for each service
"""
def __init__(self, cache_dir):
self.cache_dir = cache_dir
self.cache = defaultdict(dict)
self.lock = threading.RLock()
def __del__(self):
for service_name in self.cache:
self.save(service_name)
def cache_location(self, service_name):
return os.path.join(self.cache_dir, 'subliminal_%s.cache' % service_name)
def load(self, service_name):
with self.lock:
if service_name in self.cache:
# already loaded
return
self.cache[service_name] = defaultdict(dict)
filename = self.cache_location(service_name)
logger.debug(u'Cache: loading cache from %s' % filename)
try:
self.cache[service_name] = pickle.load(open(filename, 'rb'))
except IOError:
logger.info('Cache: Cache file "%s" doesn\'t exist, creating it' % filename)
except EOFError:
logger.error('Cache: cache file "%s" is corrupted... Removing it.' % filename)
os.remove(filename)
def save(self, service_name):
filename = self.cache_location(service_name)
logger.debug(u'Cache: saving cache to %s' % filename)
with self.lock:
pickle.dump(self.cache[service_name], open(filename, 'wb'))
def clear(self, service_name):
try:
os.remove(self.cache_location(service_name))
except OSError:
pass
self.cache[service_name] = defaultdict(dict)
def cached_func_key(self, func, cls=None):
try:
cls = func.im_class
except:
pass
return ('%s.%s' % (cls.__module__, cls.__name__), func.__name__)
def function_cache(self, service_name, func):
func_key = self.cached_func_key(func)
return self.cache[service_name][func_key]
def cache_for(self, service_name, func, args, result):
# no need to lock here, dict ops are atomic
self.function_cache(service_name, func)[args] = result
def cached_value(self, service_name, func, args):
"""Raises KeyError if not found"""
# no need to lock here, dict ops are atomic
return self.function_cache(service_name, func)[args]
def cachedmethod(function):
"""Decorator to make a method use the cache.
.. note::
This can NOT be used with static functions, it has to be used on
methods of some class
"""
@wraps(function)
def cached(*args):
c = args[0].config.cache
service_name = args[0].__class__.__name__
func_key = c.cached_func_key(function, cls=args[0].__class__)
func_cache = c.cache[service_name][func_key]
# we need to remove the first element of args for the key, as it is the
# instance pointer and we don't want the cache to know which instance
# called it, it is shared among all instances of the same class
key = args[1:]
if key in func_cache:
result = func_cache[key]
logger.debug(u'Using cached value for %s(%s), returns: %s' % (func_key, key, result))
return result
result = function(*args)
# note: another thread could have already cached a value in the
# meantime, but that's ok as we prefer to keep the latest value in
# the cache
func_cache[key] = result
return result
return cached
+270
View File
@@ -0,0 +1,270 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from .exceptions import DownloadFailedError
from .services import ServiceConfig
from .tasks import DownloadTask, ListTask
from .utils import get_keywords
from .videos import Episode, Movie, scan
from collections import defaultdict
from itertools import groupby
import bs4
import guessit
import logging
__all__ = ['SERVICES', 'LANGUAGE_INDEX', 'SERVICE_INDEX', 'SERVICE_CONFIDENCE', 'MATCHING_CONFIDENCE',
'create_list_tasks', 'create_download_tasks', 'consume_task', 'matching_confidence',
'key_subtitles', 'group_by_video']
logger = logging.getLogger(__name__)
SERVICES = ['opensubtitles', 'bierdopje', 'subswiki', 'subtitulos', 'thesubdb', 'addic7ed', 'tvsubtitles']
LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE = range(4)
def create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter):
"""Create a list of :class:`~subliminal.tasks.ListTask` from one or more paths using the given criteria
:param paths: path(s) to video file or folder
:type paths: string or list
:param set languages: languages to search for
:param list services: services to use for the search
:param bool force: force searching for subtitles even if some are detected
:param bool multi: search multiple languages for the same video
:param string cache_dir: path to the cache directory to use
:param int max_depth: maximum depth for scanning entries
:param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
:return: the created tasks
:rtype: list of :class:`~subliminal.tasks.ListTask`
"""
scan_result = []
for p in paths:
scan_result.extend(scan(p, max_depth, scan_filter))
logger.debug(u'Found %d videos in %r with maximum depth %d' % (len(scan_result), paths, max_depth))
tasks = []
config = ServiceConfig(multi, cache_dir)
services = filter_services(services)
for video, detected_subtitles in scan_result:
detected_languages = set(s.language for s in detected_subtitles)
wanted_languages = languages.copy()
if not force and multi:
wanted_languages -= detected_languages
if not wanted_languages:
logger.debug(u'No need to list multi subtitles %r for %r because %r detected' % (languages, video, detected_languages))
continue
if not force and not multi and None in detected_languages:
logger.debug(u'No need to list single subtitles %r for %r because one detected' % (languages, video))
continue
logger.debug(u'Listing subtitles %r for %r with services %r' % (wanted_languages, video, services))
for service_name in services:
mod = __import__('services.' + service_name, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
service = mod.Service
if not service.check_validity(video, wanted_languages):
continue
task = ListTask(video, wanted_languages & service.languages, service_name, config)
logger.debug(u'Created task %r' % task)
tasks.append(task)
return tasks
def create_download_tasks(subtitles_by_video, multi):
"""Create a list of :class:`~subliminal.tasks.DownloadTask` from a list results grouped by video
:param subtitles_by_video: :class:`~subliminal.tasks.ListTask` results grouped by video and sorted
:type subtitles_by_video: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.Subtitle`]
:param order: preferred order for subtitles sorting
:type list: list of :data:`LANGUAGE_INDEX`, :data:`SERVICE_INDEX`, :data:`SERVICE_CONFIDENCE`, :data:`MATCHING_CONFIDENCE`
:param bool multi: download multiple languages for the same video
:return: the created tasks
:rtype: list of :class:`~subliminal.tasks.DownloadTask`
"""
tasks = []
for video, subtitles in subtitles_by_video.iteritems():
if not subtitles:
continue
if not multi:
task = DownloadTask(video, list(subtitles))
logger.debug(u'Created task %r' % task)
tasks.append(task)
continue
for _, by_language in groupby(subtitles, lambda s: s.language):
task = DownloadTask(video, list(by_language))
logger.debug(u'Created task %r' % task)
tasks.append(task)
return tasks
def consume_task(task, services=None):
"""Consume a task. If the ``services`` parameter is given, the function will attempt
to get the service from it. In case the service is not in ``services``, it will be initialized
and put in ``services``
:param task: task to consume
:type task: :class:`~subliminal.tasks.ListTask` or :class:`~subliminal.tasks.DownloadTask`
:param dict services: mapping between the service name and an instance of this service
:return: the result of the task
:rtype: list of :class:`~subliminal.subtitles.ResultSubtitle`
"""
if services is None:
services = {}
logger.info(u'Consuming %r' % task)
result = None
if isinstance(task, ListTask):
service = get_service(services, task.service, config=task.config)
result = service.list(task.video, task.languages)
elif isinstance(task, DownloadTask):
for subtitle in task.subtitles:
service = get_service(services, subtitle.service)
try:
service.download(subtitle)
result = [subtitle]
break
except DownloadFailedError:
logger.warning(u'Could not download subtitle %r, trying next' % subtitle)
continue
if result is None:
logger.error(u'No subtitles could be downloaded for video %r' % task.video)
return result
def matching_confidence(video, subtitle):
"""Compute the probability (confidence) that the subtitle matches the video
:param video: video to match
:type video: :class:`~subliminal.videos.Video`
:param subtitle: subtitle to match
:type subtitle: :class:`~subliminal.subtitles.Subtitle`
:return: the matching probability
:rtype: float
"""
guess = guessit.guess_file_info(subtitle.release, 'autodetect')
video_keywords = get_keywords(video.guess)
subtitle_keywords = get_keywords(guess) | subtitle.keywords
replacement = {'keywords': len(video_keywords & subtitle_keywords)}
if isinstance(video, Episode):
replacement.update({'series': 0, 'season': 0, 'episode': 0})
matching_format = '{series:b}{season:b}{episode:b}{keywords:03b}'
best = matching_format.format(series=1, season=1, episode=1, keywords=len(video_keywords))
if guess['type'] in ['episode', 'episodesubtitle']:
if 'series' in guess and guess['series'].lower() == video.series.lower():
replacement['series'] = 1
if 'season' in guess and guess['season'] == video.season:
replacement['season'] = 1
if 'episodeNumber' in guess and guess['episodeNumber'] == video.episode:
replacement['episode'] = 1
elif isinstance(video, Movie):
replacement.update({'title': 0, 'year': 0})
matching_format = '{title:b}{year:b}{keywords:03b}'
best = matching_format.format(title=1, year=1, keywords=len(video_keywords))
if guess['type'] in ['movie', 'moviesubtitle']:
if 'title' in guess and guess['title'].lower() == video.title.lower():
replacement['title'] = 1
if 'year' in guess and guess['year'] == video.year:
replacement['year'] = 1
else:
return 0
confidence = float(int(matching_format.format(**replacement), 2)) / float(int(best, 2))
return confidence
def get_service(services, service_name, config=None):
"""Get a service from its name in the service dict with the specified config.
If the service does not exist in the service dict, it is created and added to the dict.
:param dict services: dict where to get existing services or put created ones
:param string service_name: name of the service to get
:param config: config to use for the service
:type config: :class:`~subliminal.services.ServiceConfig` or None
:return: the corresponding service
:rtype: :class:`~subliminal.services.ServiceBase`
"""
if service_name not in services:
mod = __import__('services.' + service_name, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
services[service_name] = mod.Service()
services[service_name].init()
services[service_name].config = config
return services[service_name]
def key_subtitles(subtitle, video, languages, services, order):
"""Create a key to sort subtitle using the given order
:param subtitle: subtitle to sort
:type subtitle: :class:`~subliminal.subtitles.ResultSubtitle`
:param video: video to match
:type video: :class:`~subliminal.videos.Video`
:param list languages: languages in preferred order
:param list services: services in preferred order
:param order: preferred order for subtitles sorting
:type list: list of :data:`LANGUAGE_INDEX`, :data:`SERVICE_INDEX`, :data:`SERVICE_CONFIDENCE`, :data:`MATCHING_CONFIDENCE`
:return: a key ready to use for subtitles sorting
:rtype: int
"""
key = ''
for sort_item in order:
if sort_item == LANGUAGE_INDEX:
key += '{0:03d}'.format(len(languages) - languages.index(subtitle.language) - 1)
key += '{0:01d}'.format(subtitle.language == languages[languages.index(subtitle.language)])
elif sort_item == SERVICE_INDEX:
key += '{0:02d}'.format(len(services) - services.index(subtitle.service) - 1)
elif sort_item == SERVICE_CONFIDENCE:
key += '{0:04d}'.format(int(subtitle.confidence * 1000))
elif sort_item == MATCHING_CONFIDENCE:
confidence = 0
if subtitle.release:
confidence = matching_confidence(video, subtitle)
key += '{0:04d}'.format(int(confidence * 1000))
return int(key)
def group_by_video(list_results):
"""Group the results of :class:`ListTasks <subliminal.tasks.ListTask>` into a
dictionary of :class:`~subliminal.videos.Video` => :class:`~subliminal.subtitles.Subtitle`
:param list_results:
:type list_results: list of result of :class:`~subliminal.tasks.ListTask`
:return: subtitles grouped by videos
:rtype: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.Subtitle`]
"""
result = defaultdict(list)
for video, subtitles in list_results:
result[video] += subtitles
return result
def filter_services(services):
"""Filter out services that are not available because of a missing feature
:param list services: service names to filter
:return: a copy of the initial list of service names without unavailable ones
:rtype: list
"""
filtered_services = services[:]
for service_name in services:
mod = __import__('services.' + service_name, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
service = mod.Service
if service.required_features is not None and bs4.builder_registry.lookup(*service.required_features) is None:
logger.warning(u'Service %s not available: none of available features could be used. One of %r required' % (service_name, service.required_features))
filtered_services.remove(service_name)
return filtered_services
-66
View File
@@ -1,66 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 Nic Wolfe <nic@wolfeden.ca>
# 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 os
import subliminal
# This module tries to deal with the apparently random behavior of python when dealing with unicode <-> utf-8
# encodings. It tries to just use unicode, but if that fails then it tries forcing it to utf-8. Any functions
# which return something should always return unicode.
def fixStupidEncodings(x, silent=False):
if type(x) == str:
try:
return x.decode(subliminal.SYS_ENCODING)
except UnicodeDecodeError:
subliminal.logger.error(u"Unable to decode value: " + repr(x))
return None
elif type(x) == unicode:
return x
else:
subliminal.logger.log(u"Unknown value passed in, ignoring it: " + str(type(x)) + " (" + repr(x) + ":" + repr(type(x)) + ")", logging.DEBUG if silent else logging.ERROR)
return None
return None
def fixListEncodings(x):
if type(x) != list:
return x
else:
return filter(lambda x: x != None, map(fixStupidEncodings, x))
def ek(func, *args):
result = None
if os.name == 'nt':
result = func(*args)
else:
result = func(*[x.encode(subliminal.SYS_ENCODING) if type(x) in (str, unicode) else x for x in args])
if type(result) == list:
return fixListEncodings(result)
elif type(result) == str:
return fixStupidEncodings(result)
else:
return result
+32
View File
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
class Error(Exception):
"""Base class for exceptions in subliminal"""
pass
class ServiceError(Error):
""""Exception raised by services"""
pass
class DownloadFailedError(Error):
""""Exception raised when a download task has failed in service"""
pass
+18
View File
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
__version__ = '0.6.0'
File diff suppressed because it is too large Load Diff
-158
View File
@@ -1,158 +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 PluginBase
import zipfile
import os
import urllib2
import urllib
import traceback
import httplib
import re
import socket
class Addic7ed(PluginBase.PluginBase):
site_url = 'http://www.addic7ed.com'
site_name = 'Addic7ed'
server_url = 'http://www.addic7ed.com'
multi_languages_queries = True
multi_filename_queries = False
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, filenames, languages):
''' Main method to call when you want to list subtitles '''
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
if not self.checkLanguages(languages):
return []
filepath = filenames[0]
guess = guessit.guess_file_info(filepath, 'autodetect')
if guess['type'] != 'episode':
return []
# add multiple things to the release group set
release_group = set()
if 'releaseGroup' in guess:
release_group.add(guess['releaseGroup'])
else:
if 'title' in guess:
release_group.add(guess['title'])
if 'screenSize' in guess:
release_group.add(guess['screenSize'])
if 'series' not in guess or len(release_group) == 0:
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):
''' Make a query and returns info about found subtitles '''
searchname = name.lower().replace(" ", "_")
searchurl = "%s/serie/%s/%s/%s/%s" % (self.server_url, searchname, season, episode, 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]], [".", "_", " "])
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 = {}
result["release"] = "%s.S%.2dE%.2d.%s" % (name.replace(" ", "."), int(season), int(episode), '.'.join(sub_teams))
result["lang"] = sub_language
result["link"] = sub_link
result["page"] = searchurl
result["filename"] = filepath
result["plugin"] = self.getClassName()
result["teams"] = sub_teams # used to sort
sublinks.append(result)
sublinks.sort(self._cmpTeams)
return sublinks
def download(self, subtitle):
'''pass the URL of the sub and the file it matches, will unzip it
and return the path to the created file'''
suburl = subtitle["link"]
videofilename = subtitle["filename"]
srtbasefilename = videofilename.rsplit(".", 1)[0]
srtfilename = srtbasefilename + self.getExtension(subtitle)
self.downloadFile(suburl, srtfilename)
return srtfilename
def _cmpTeams(self, x, y):
''' Sort based on teams matching '''
return -cmp(len(x['teams'].intersection(self.release_group)), len(y['teams'].intersection(self.release_group)))
-170
View File
@@ -1,170 +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
import pickle
import traceback
import urllib
import urllib2
from subliminal import encodingKludge as ek
class BierDopje(PluginBase.PluginBase):
site_url = 'http://bierdopje.com'
site_name = 'BierDopje'
server_url = 'http://api.bierdopje.com/A2B638AC5D804C2E/'
multi_languages_queries = True
multi_filename_queries = False
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):
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 not config_dict or not config_dict['cache_dir']:
raise Exception('Cache directory is mandatory for this plugin')
self.showid_cache = ek.ek(os.path.join, config_dict['cache_dir'], "bierdopje_showid.cache")
with self.lock:
if not ek.ek(os.path.exists, self.showid_cache):
if not ek.ek(os.path.exists, ek.ek(os.path.dirname, self.showid_cache)):
raise Exception("Cache directory doesn't exists")
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, filenames, languages):
"""Main method to call when you want to list subtitles"""
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
if not self.checkLanguages(languages):
return []
filepath = filenames[0]
guess = guessit.guess_file_info(filepath, 'autodetect')
if guess['type'] != '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:
return []
self.release_group = release_group # used to sort results
return self.query(guess['series'], guess['season'], guess['episodeNumber'], release_group, filepath, languages)
def download(self, subtitle):
"""Main method to call when you want to download a subtitle"""
subpath = subtitle["filename"].rsplit(".", 1)[0] + self.getExtension(subtitle)
self.downloadFile(subtitle["link"], subpath)
return subpath
def query(self, name, season, episode, release_group, filepath, languages=None):
"""Makes a query and returns info (link, lang) about found subtitles"""
if languages:
available_languages = list(set(languages).intersection((self._plugin_languages.values())))
else:
available_languages = self._plugin_languages.values()
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_id_url = "%sGetShowByName/%s" % (self.server_url, urllib.quote(show_name))
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 available_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 = {}
result["release"] = sub_release
result["link"] = sub_link
result["page"] = sub_link
result["lang"] = language
result["filename"] = filepath
result["plugin"] = self.getClassName()
result["releaseGroup"] = sub_release_group
sublinks.append(result)
sublinks.sort(self._cmpReleaseGroup)
return sublinks
def _cmpReleaseGroup(self, x, y):
"""Sort based on teams matching"""
return -cmp(len(x['releaseGroup'].intersection(self.release_group)), len(y['releaseGroup'].intersection(self.release_group)))
-206
View File
@@ -1,206 +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
from subliminal import encodingKludge as ek
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.3'
multi_languages_queries = True
multi_filename_queries = False
api_based = True
_plugin_languages = {"en": "eng",
"fr": "fre",
"hu": "hun",
"cs": "cze",
"pl": "pol",
"sk": "slo",
"pt": "por",
"pt-br": "pob",
"es": "spa",
"el": "ell",
"ar": "ara",
"sq": "alb",
"hy": "arm",
"ay": "ass",
"bs": "bos",
"bg": "bul",
"ca": "cat",
"zh": "chi",
"hr": "hrv",
"da": "dan",
"nl": "dut",
"eo": "epo",
"et": "est",
"fi": "fin",
"gl": "glg",
"ka": "geo",
"de": "ger",
"he": "heb",
"hi": "hin",
"is": "ice",
"id": "ind",
"it": "ita",
"ja": "jpn",
"kk": "kaz",
"ko": "kor",
"lv": "lav",
"lt": "lit",
"lb": "ltz",
"mk": "mac",
"ms": "may",
"no": "nor",
"oc": "oci",
"fa": "per",
"ro": "rum",
"ru": "rus",
"sr": "scc",
"sl": "slv",
"sv": "swe",
"th": "tha",
"tr": "tur",
"uk": "ukr",
"vi": "vie"}
def __init__(self, config_dict=None):
super(OpenSubtitles, self).__init__(self._plugin_languages, config_dict)
def list(self, filenames, languages):
"""Main method to call when you want to list subtitles """
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
filepath = filenames[0]
if ek.ek(os.path.isfile, filepath):
filehash = self.hashFile(filepath)
size = ek.ek(os.path.getsize, filepath)
return self.query(moviehash=filehash, languages=languages, bytesize=size, filepath=filepath)
else:
return self.query(languages=languages, filepath=filepath)
def download(self, subtitle):
"""Main method to call when you want to download a subtitle """
subtitleFilename = subtitle["filename"].rsplit(".", 1)[0] + self.getExtension(subtitle)
self.downloadFile(subtitle["link"], subtitleFilename + ".gz")
f = ek.ek(gzip.open, subtitleFilename + ".gz")
dump = ek.ek(open, subtitleFilename, "wb")
dump.write(f.read())
self.adjustPermissions(subtitleFilename)
dump.close()
f.close()
ek.ek(os.remove, subtitleFilename + ".gz")
return subtitleFilename
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, we'll use the filename")
guess = guessit.guess_file_info(filepath, 'autodetect')
if guess['type'] == 'episode' and 'series' in guess:
search['query'] = guess['series']
elif guess['type'] == 'movie':
search['query'] = guess['title']
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, e:
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 = {}
result["release"] = r['SubFileName']
result["link"] = r['SubDownloadLink']
result["page"] = r['SubDownloadLink']
result["lang"] = self.getRevertLanguage(r['SubLanguageID'])
result["filename"] = filepath
result["plugin"] = self.getClassName()
if 'query' in search and not r["MovieReleaseName"].replace('.', ' ').startswith(search['query']): # query mode search, filter results
self.logger.debug(u"Skipping %s it does not start with %s" % (r["MovieReleaseName"].replace('.', ' '), search['query']))
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
-188
View File
@@ -1,188 +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 re
import sys
import urllib2
import struct
import threading
from subliminal import encodingKludge as ek
class PluginBase(object):
__metaclass__ = abc.ABCMeta
multi_languages_queries = False
multi_filename_queries = False
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.getClassName())
@staticmethod
def getFileName(filepath):
filename = filepath
if ek.ek(os.path.isfile, filename):
filename = ek.ek(os.path.basename, filename)
if filename.endswith(('.avi', '.wmv', '.mov', '.mp4', '.mpeg', '.mpg', '.mkv')):
filename = filename.rsplit('.', 1)[0]
return filename
def hashFile(self, filename):
"""Hash a file like OpenSubtitles"""
longlongformat = 'q' # long long
bytesize = struct.calcsize(longlongformat)
f = ek.ek(open, filename, "rb")
filesize = ek.ek(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 x 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 x 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, filename, data=None):
"""Downloads the given url to the given filename"""
try:
self.logger.info(u"Downloading %s" % url)
req = urllib2.Request(url, headers={'Referer': url, 'User-Agent': self.user_agent})
f = urllib2.urlopen(req, data=data)
dump = ek.ek(open, filename, "wb")
dump.write(f.read())
self.adjustPermissions(filename)
dump.close()
f.close()
self.logger.debug(u"Download finished for file %s. Size: %s" % (filename, ek.ek(os.path.getsize, filename)))
except urllib2.HTTPError, e:
self.logger.error(u"HTTP Error:", e.code, url)
except urllib2.URLError, e:
self.logger.error(u"URL Error:", e.reason, url)
def adjustPermissions(self, filepath):
if self.config_dict and 'files_mode' in self.config_dict and self.config_dict['files_mode'] != -1:
ek.ek(os.chmod, filepath, self.config_dict['files_mode'])
@abc.abstractmethod
def list(self, filenames, languages):
"""Main method to call when you want to list subtitles"""
@abc.abstractmethod
def download(self, subtitle):
"""Main method to call when you want to download a subtitle"""
def getRevertLanguage(self, language):
"""Returns the short (two-character) representation from the long language name"""
try:
return self.revertPluginLanguages[language]
except KeyError, e:
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.getClassName(), language))
def checkLanguages(self, languages):
if languages and not set(languages).intersection((self._plugin_languages.values())):
self.logger.debug(u'None of requested languages %s are available' % languages)
return False
return True
def getLanguage(self, language):
"""Returns the long naming of the language from a two character code"""
try:
return self.pluginLanguages[language]
except KeyError, e:
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.getClassName(), language))
def getExtension(self, subtitle):
if self.config_dict and self.config_dict['multi']:
return ".%s.srt" % subtitle['lang']
return ".srt"
def getClassName(self):
return self.__class__.__name__
def splitTask(self, task):
"""Determines if the plugin can handle multi-thing queries and output splited tasks for list task only"""
if task['task'] != 'list':
return [task]
tasks = [task]
if not self.multi_filename_queries:
tasks = self._splitOnField(tasks, 'filenames')
if not self.multi_languages_queries:
tasks = self._splitOnField(tasks, 'languages')
return tasks
@staticmethod
def _splitOnField(elements, field):
"""
Split a list of dict in a bigger one if the element field in the dict has multiple elements too
i.e. [{'a': 1, 'b': [2,3]}, {'a': 7, 'b': [4]}] => [{'a': 1, 'b': [2]}, {'a': 1, 'b': [3]}, {'a': 7, 'b': [4]}]
with field = 'b'
"""
results = []
for e in elements:
for v in e[field]:
newElement = {}
for (key, value) in e.items():
if key != field:
newElement[key] = value
else:
newElement[key] = [v]
results.append(newElement)
return results
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
-146
View File
@@ -1,146 +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 struct
import socket
import zipfile
import os
import urllib2
import urllib
import traceback
from subliminal import encodingKludge as ek
class Podnapisi(PluginBase.PluginBase):
site_url = "http://www.podnapisi.net"
site_name = "Podnapisi"
server_url = 'http://ssp.podnapisi.net:8000'
multi_languages_queries = True
multi_filename_queries = False
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"""
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
filepath = filenames[0]
if not ek.ek(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, e:
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)
d = self.server.download(token, [173793])
self.server.terminate(token)
return subs
-177
View File
@@ -1,177 +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
import urllib
import traceback
import httplib
from subliminal import encodingKludge as ek
class SubScene(PluginBase.PluginBase):
site_url = 'http://subscene.com'
site_name = 'SubScene'
server_url = 'http://subscene.com/s.aspx?subtitle='
multi_languages_queries = True
multi_filename_queries = False
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"""
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
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 = ek.ek(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()
ek.ek(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, ek.ek(os.path.dirname, archivefilename)]
subprocess.Popen(args)
tmpsubtitlefilename = ek.ek(os.path.join, ek.ek(os.path.dirname, archivefilename), el)
subtitlefilename = ek.ek(os.path.join, ek.ek(os.path.dirname, archivefilename), srtbasefilename + "." + extension)
if ek.ek(os.path.exists, tmpsubtitlefilename):
# rename it to match the file
ek.ek(os.rename, tmpsubtitlefilename, subtitlefilename)
# exit
return subtitlefilename
except OSError, 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
super(SubScene, self).downloadFile(url, filename, urllib.urlencode({'__EVENTTARGET': 's$lc$bcr$downloadLink', '__EVENTARGUMENT': '', '__VIEWSTATE': '/wEPDwUHNzUxOTkwNWRk4wau5efPqhlBJJlOkKKHN8FIS04='}))
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, urllib.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.getClassName()
sublinks.append(result)
return sublinks
-156
View File
@@ -1,156 +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 urllib2
import urllib
import logging
import traceback
import httplib
import re
import guessit
from subliminal import encodingKludge as ek
class SubsWiki(PluginBase.PluginBase):
site_url = 'http://www.subswiki.com'
site_name = 'SubsWiki'
server_url = 'http://www.subswiki.com'
multi_languages_queries = True
multi_filename_queries = False
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, filenames, languages):
"""Main method to call when you want to list subtitles"""
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
filepath = filenames[0]
if not self.checkLanguages(languages):
return []
guess = guessit.guess_file_info(filepath, 'autodetect')
if guess['type'] != 'episode':
return []
# add multiple things to the release group set
release_group = set()
if 'releaseGroup' in guess:
release_group.add(guess['releaseGroup'])
else:
if 'title' in guess:
release_group.add(guess['title'])
if 'screenSize' in guess:
release_group.add(guess['screenSize'])
if 'series' not in guess or len(release_group) == 0:
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):
"""Make a query and returns info about found subtitles"""
sublinks = []
searchname = name.lower().replace(" ", "_")
searchurl = "%s/serie/%s/%s/%s/" % (self.server_url, 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)], [".", "_", " ", "/", "-"])
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 languages and 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 = {}
result["release"] = "%s.S%.2dE%.2d.%s" % (name.replace(" ", "."), int(season), int(episode), '.'.join(sub_teams))
result["lang"] = sub_language
result["link"] = self.server_url + sub_link
result["page"] = searchurl
result["filename"] = filepath
result["plugin"] = self.getClassName()
result["teams"] = sub_teams # used to sort
sublinks.append(result)
sublinks.sort(self._cmpTeams)
return sublinks
def download(self, subtitle):
"""Main method to call when you want to download a subtitle"""
subtitleFilename = subtitle["filename"].rsplit(".", 1)[0] + self.getExtension(subtitle)
self.downloadFile(subtitle["link"], subtitleFilename)
return subtitleFilename
def listTeams(self, subteams, separators):
teams = []
for sep in separators:
subteams = self.splitTeam(subteams, sep)
return set(subteams)
def splitTeam(self, subteams, sep):
teams = []
for t in subteams:
teams += t.split(sep)
return teams
def downloadFile(self, url, filename):
"""Downloads the given url to the given filename"""
req = urllib2.Request(url, headers={'Referer': url, 'User-Agent': 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.3)'})
f = urllib2.urlopen(req)
dump = ek.ek(open, filename, "wb")
dump.write(f.read())
dump.close()
f.close()
def _cmpTeams(self, x, y):
"""Sort based on teams matching"""
return -cmp(len(x['teams'].intersection(self.release_group)), len(y['teams'].intersection(self.release_group)))
-122
View File
@@ -1,122 +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 ConfigParser
import PluginBase
import traceback
import urllib
import urllib2
import xml.dom.minidom
class SubtitleSource(PluginBase.PluginBase):
site_url = 'http://www.subtitlesource.org'
site_name = 'SubtitleSource'
server_url = 'http://www.subtitlesource.org/api/%s/3.0/xmlsearch'
multi_languages_queries = True
multi_filename_queries = False
api_based = True
_plugin_languages = {"en": "English",
"sv": "Swedish",
"da": "Danish",
"fi": "Finnish",
"no": "Norwegian",
"fr": "French",
"es": "Spanish",
"is": "Icelandic"}
def __init__(self, config_dict=None):
super(SubtitleSource, self).__init__(self._plugin_languages, config_dict)
if config_dict and "subtitlesource_key" in config_dict:
self.server_url = self.server_url % config_dict["subtitlesource_key"]
else:
self.logger.error(u'SubtitleSource API Key is mandatory for this plugin')
raise Exception('SubtitleSource API Key is mandatory for this plugin')
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, 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, languages)
return subs
else:
return subs
def query(self, token, languages=None):
"""Makes a query on SubtitlesSource and returns info (link, lang) about found subtitles"""
self.logger.debug(u"Local file is: %s " % token)
sublinks = []
if not languages: # langs is empty of None
languages = ["all"]
else: # parse each lang to generate the equivalent lang
languages = [self._plugin_languages[l] for l in languages if l in self._plugin_languages.keys()]
# Get the CD part of this
metaData = self.guessFileData(token)
multipart = metaData.get('part', None)
part = metaData.get('part')
if not part: # part will return None if not found using the regex
part = 1
for lang in languages:
searchurl = "%s/%s/%s/0" % (self.server_url, urllib.quote(token), lang)
self.logger.debug(u"dl'ing %s" % searchurl)
page = urllib2.urlopen(searchurl, timeout=self.timeout)
xmltree = xml.dom.minidom.parse(page)
subs = xmltree.getElementsByTagName("sub")
for sub in subs:
sublang = self.getRevertLanguage(self.getValue(sub, "language"))
if languages and not sublang in languages:
continue # The language of this sub is not wanted => Skip
if multipart and not int(self.getValue(sub, 'cd')) > 1:
continue # The subtitle is not a multipart
dllink = "http://www.subtitlesource.org/download/text/%s/%s" % (self.getValue(sub, "id"), part)
self.logger.debug(u"Link added: %s (%s)" % (dllink, sublang))
result = {}
result["release"] = self.getValue(sub, "releasename")
result["link"] = dllink
result["page"] = dllink
result["lang"] = sublang
releaseMetaData = self.guessFileData(result['release'])
teams = set(metaData['teams'])
srtTeams = set(releaseMetaData['teams'])
self.logger.debug(u"Analyzing: %s " % result['release'])
self.logger.debug(u"Local file has: %s " % metaData['teams'])
self.logger.debug(u"Remote sub has: %s " % releaseMetaData['teams'])
if result['release'].startswith(token) or (releaseMetaData['name'] == metaData['name'] and releaseMetaData['type'] == metaData['type'] and (teams.issubset(srtTeams) or srtTeams.issubset(teams))):
sublinks.append(result)
return sublinks
def download(self, subtitle):
"""Main method to call when you want to download a subtitle"""
suburl = subtitle["link"]
videofilename = subtitle["filename"]
srtfilename = videofilename.rsplit(".", 1)[0] + self.getExtension(subtitle)
self.downloadFile(suburl, srtfilename)
return srtfilename
def getValue(self, sub, tagName):
for node in sub.childNodes:
if node.nodeType == node.ELEMENT_NODE and node.tagName == tagName:
return node.childNodes[0].nodeValue
-150
View File
@@ -1,150 +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 zipfile
import urllib2
import urllib
import logging
import traceback
import httplib
import re
import PluginBase
from subliminal import encodingKludge as ek
class Subtitulos(PluginBase.PluginBase):
site_url = 'http://www.subtitulos.es'
site_name = 'Subtitulos'
server_url = 'http://www.subtitulos.es'
multi_languages_queries = True
multi_filename_queries = False
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, filenames, languages):
"""Main method to call when you want to list subtitles"""
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
if not self.checkLanguages(languages):
return []
filepath = filenames[0]
guess = guessit.guess_file_info(filepath, 'autodetect')
if guess['type'] != '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:
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):
"""Make a query and returns info about found subtitles"""
sublinks = []
searchname = name.lower().replace(" ", "-")
searchurl = "%s/%s/%sx%.2d" % (self.server_url, 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()], [".", "_", " ", "/"])
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.findAllNext("ul", {"class": "sslist"}):
sub_language = self.getRevertLanguage(html_language.findNext("li", {"class": "li-idioma"}).find("strong").contents[0].string.strip())
if languages and 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 = {}
result["release"] = "%s.S%.2dE%.2d.%s" % (name.replace(" ", "."), int(season), int(episode), '.'.join(sub_teams))
result["lang"] = sub_language
result["link"] = sub_link
result["page"] = searchurl
result["filename"] = filepath
result["plugin"] = self.getClassName()
result["teams"] = sub_teams # used to sort
sublinks.append(result)
sublinks.sort(self._cmpTeams)
return sublinks
def download(self, subtitle):
"""
Pass the URL of the sub and the file it matches, will unzip it
and return the path to the created file
"""
suburl = subtitle["link"]
videofilename = subtitle["filename"]
srtbasefilename = videofilename.rsplit(".", 1)[0]
srtfilename = srtbasefilename + ".srt"
self.downloadFile(suburl, srtfilename)
return srtfilename
def downloadFile(self, url, filename):
"""Downloads the given url to the given filename"""
req = urllib2.Request(url, headers={'Referer': url, 'User-Agent': 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.3)'})
f = urllib2.urlopen(req)
dump = ek.ek(open, filename, "wb")
dump.write(f.read())
dump.close()
f.close()
def _cmpTeams(self, x, y):
"""Sort based on teams matching"""
return -cmp(len(x['teams'].intersection(self.release_group)), len(y['teams'].intersection(self.release_group)))
-125
View File
@@ -1,125 +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 import encodingKludge as ek
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
multi_languages_queries = True
multi_filename_queries = False
api_based = True
user_agent = 'SubDB/1.0 (Subliminal/0.1; https://github.com/Diaoul/subliminal)' # defined by the API
_plugin_languages = {'cs': 'cs', # the whole list is available with the API: http://sandbox.thesubdb.com/?action=languages
'da': 'da',
'de': 'de',
'en': 'en',
'fi': 'fi',
'fr': 'fr',
'hu': 'hu',
'id': 'id',
'it': 'it',
'nl': 'nl',
'no': 'no',
'pl': 'pl',
'pt': 'pt',
'ro': 'ro',
'ru': 'ru',
'sl': 'sl',
'sr': 'sr',
'sv': 'sv',
'tr': 'tr'}
def __init__(self, config_dict=None):
super(TheSubDB, self).__init__(self._plugin_languages, config_dict)
def list(self, filenames, languages):
"""Main method to call when you want to list subtitles"""
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
filepath = filenames[0]
if not ek.ek(os.path.isfile, filepath):
return []
return self.query(filepath, self.hashFile(filepath), languages)
def query(self, filepath, moviehash, languages=None):
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 not languages or l in languages:
result = {}
result['release'] = filepath
result['lang'] = l
result['link'] = "%s/?action=download&hash=%s&language=%s" % (self.server_url, moviehash, l)
result['page'] = result['link']
result['filename'] = filepath
result['plugin'] = self.getClassName()
subs.append(result)
return subs
def hashFile(self, name):
"""This hash function receives the filename and returns the hash code"""
readsize = 64 * 1024
with ek.ek(open, name, 'rb') as f:
size = ek.ek(os.path.getsize, name)
data = f.read(readsize)
f.seek(-readsize, os.SEEK_END)
data += f.read(readsize)
return hashlib.md5(data).hexdigest()
def download(self, subtitle):
"""Main method to call when you want to download a subtitle"""
suburl = subtitle["link"]
videofilename = subtitle["filename"]
srtfilename = videofilename.rsplit(".", 1)[0] + self.getExtension(subtitle)
self.downloadFile(suburl, srtfilename)
return srtfilename
def downloadFile(self, url, srtfilename):
"""Downloads the given url to the given filename"""
req = urllib2.Request(url)
req.add_header('User-Agent', self.user_agent)
f = urllib2.urlopen(req)
dump = open(srtfilename, "wb")
dump.write(f.read())
dump.close()
f.close()
-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 SubtitleSource import SubtitleSource
from Subtitulos import Subtitulos
from TheSubDB import TheSubDB
+256
View File
@@ -0,0 +1,256 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from ..cache import Cache
from ..exceptions import DownloadFailedError, ServiceError
from ..language import language_set, Language
from ..subtitles import EXTENSIONS
import logging
import os
import requests
import threading
import zipfile
__all__ = ['ServiceBase', 'ServiceConfig']
logger = logging.getLogger(__name__)
class ServiceBase(object):
"""Service base class
:param config: service configuration
:type config: :class:`ServiceConfig`
"""
#: URL to the service server
server_url = ''
#: User Agent for any HTTP-based requests
user_agent = 'subliminal v0.6'
#: Whether based on an API or not
api_based = False
#: Timeout for web requests
timeout = 5
#: :class:`~subliminal.language.language_set` of available languages
languages = language_set()
#: Map between language objects and language codes used in the service
language_map = {}
#: Default attribute of a :class:`~subliminal.language.Language` to get with :meth:`get_code`
language_code = 'alpha2'
#: Accepted video classes (:class:`~subliminal.videos.Episode`, :class:`~subliminal.videos.Movie`, :class:`~subliminal.videos.UnknownVideo`)
videos = []
#: Whether the video has to exist or not
require_video = False
#: List of required features for BeautifulSoup
required_features = None
def __init__(self, config=None):
self.config = config or ServiceConfig()
self.session = None
def __enter__(self):
self.init()
return self
def __exit__(self, *args):
self.terminate()
def init(self):
"""Initialize connection"""
logger.debug(u'Initializing %s' % self.__class__.__name__)
self.session = requests.session(timeout=10, headers={'User-Agent': self.user_agent})
def init_cache(self):
"""Initialize cache, make sure it is loaded from disk"""
if not self.config or not self.config.cache:
raise ServiceError('Cache directory is required')
self.config.cache.load(self.__class__.__name__)
def save_cache(self):
self.config.cache.save(self.__class__.__name__)
def clear_cache(self):
self.config.cache.clear(self.__class__.__name__)
def cache_for(self, func, args, result):
return self.config.cache.cache_for(self.__class__.__name__, func, args, result)
def cached_value(self, func, args):
return self.config.cache.cached_value(self.__class__.__name__, func, args)
def terminate(self):
"""Terminate connection"""
logger.debug(u'Terminating %s' % self.__class__.__name__)
def get_code(self, language):
"""Get the service code for a :class:`~subliminal.language.Language`
It uses the :data:`language_map` and if there's no match, falls back
on the :data:`language_code` attribute of the given :class:`~subliminal.language.Language`
"""
if language in self.language_map:
return self.language_map[language]
if self.language_code is None:
raise ValueError('%r has no matching code' % language)
return getattr(language, self.language_code)
def get_language(self, code):
"""Get a :class:`~subliminal.language.Language` from a service code
It uses the :data:`language_map` and if there's no match, uses the
given code as ``language`` parameter for the :class:`~subliminal.language.Language`
constructor
.. note::
A warning is emitted if the generated :class:`~subliminal.language.Language`
is "Undetermined"
"""
if code in self.language_map:
return self.language_map[code]
language = Language(code, strict=False)
if language == Language('Undetermined'):
logger.warning(u'Code %s could not be identified as a language for %s' % (code, self.__class__.__name__))
return language
def query(self, *args):
"""Make the actual query"""
raise NotImplementedError()
def list(self, video, languages):
"""List subtitles
As a service writer, you can either override this method or implement
:meth:`list_checked` instead to have the languages pre-filtered for you
"""
if not self.check_validity(video, languages):
return []
return self.list_checked(video, languages)
def list_checked(self, video, languages):
"""List subtitles without having to check parameters for validity"""
raise NotImplementedError()
def download(self, subtitle):
"""Download a subtitle"""
self.download_file(subtitle.link, subtitle.path)
@classmethod
def check_validity(cls, video, languages):
"""Check for video and languages validity in the Service
:param video: the video to check
:type video: :class:`~subliminal.videos.video`
:param languages: languages to check
:type languages: :class:`~subliminal.language.Language`
:rtype: bool
"""
languages = (languages & cls.languages) - language_set(['Undetermined'])
if not languages:
logger.debug(u'No language available for service %s' % cls.__name__.lower())
return False
if cls.require_video and not video.exists or not isinstance(video, tuple(cls.videos)):
logger.debug(u'%r is not valid for service %s' % (video, cls.__name__.lower()))
return False
return True
def download_file(self, url, filepath):
"""Attempt to download a file and remove it in case of failure
:param string url: URL to download
:param string filepath: destination path
"""
logger.info(u'Downloading %s' % url)
try:
r = self.session.get(url, headers={'Referer': url, 'User-Agent': self.user_agent})
with open(filepath, 'wb') as f:
f.write(r.content)
except Exception as e:
logger.error(u'Download %s failed: %s' % (url, e))
if os.path.exists(filepath):
os.remove(filepath)
raise DownloadFailedError(str(e))
logger.debug(u'Download finished for file %s. Size: %s' % (filepath, os.path.getsize(filepath)))
def download_zip_file(self, url, filepath):
"""Attempt to download a zip file and extract any subtitle file from it, if any.
This cleans up after itself if anything fails.
:param string url: URL of the zip file to download
:param string filepath: destination path for the subtitle
"""
logger.info(u'Downloading %s' % url)
try:
zippath = filepath + '.zip'
r = self.session.get(url, headers={'Referer': url, 'User-Agent': self.user_agent})
with open(zippath, 'wb') as f:
f.write(r.content)
if not zipfile.is_zipfile(zippath):
# TODO: could check if maybe we already have a text file and
# download it directly
raise DownloadFailedError('Downloaded file is not a zip file')
zipsub = zipfile.ZipFile(zippath)
for subfile in zipsub.namelist():
if os.path.splitext(subfile)[1] in EXTENSIONS:
open(filepath, 'w').write(zipsub.open(subfile).read())
break
else:
logger.debug(u'No subtitles found in zip file')
raise DownloadFailedError('No subtitles found in zip file')
os.remove(zippath)
logger.debug(u'Download finished for file %s. Size: %s' % (filepath, os.path.getsize(filepath)))
return
except Exception as e:
logger.error(u'Download %s failed: %s' % (url, e))
if os.path.exists(zippath):
os.remove(zippath)
if os.path.exists(filepath):
os.remove(filepath)
raise DownloadFailedError(str(e))
class ServiceConfig(object):
"""Configuration for any :class:`Service`
:param bool multi: whether to download one subtitle per language or not
:param string cache_dir: cache directory
"""
def __init__(self, multi=False, cache_dir=None):
self.multi = multi
self.cache_dir = cache_dir
self.cache = None
if cache_dir is not None:
self.cache = Cache(cache_dir)
def __repr__(self):
return 'ServiceConfig(%r, %s)' % (self.multi, self.cache.cache_dir)
+154
View File
@@ -0,0 +1,154 @@
# -*- coding: utf-8 -*-
# Copyright 2012 Olivier Leveau <olifozzy@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..cache import cachedmethod
from ..language import Language, language_set
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..utils import get_keywords
from ..videos import Episode
from bs4 import BeautifulSoup
import logging
import re
logger = logging.getLogger(__name__)
def match(pattern, string):
try:
return re.search(pattern, string).group(1)
except AttributeError:
logger.debug(u'Could not match %r on %r' % (pattern, string))
return None
def matches(pattern, string):
try:
return re.search(pattern, string).group(1, 2)
except AttributeError:
logger.debug(u'Could not match %r on %r' % (pattern, string))
return None
class Addic7ed(ServiceBase):
server_url = 'http://www.addic7ed.com'
api_based = False
#TODO: Complete this
languages = language_set(['ar', 'ca', 'de', 'el', 'en', 'es', 'eu', 'fr', 'ga', 'he', 'hr', 'hu', 'it',
'pl', 'pt', 'ro', 'ru', 'se', 'pt-br'])
language_map = {'Portuguese (Brazilian)': Language('por-BR'), 'Greek': Language('gre')}
videos = [Episode]
require_video = False
required_features = ['permissive']
@cachedmethod
def get_likely_series_id(self, name):
r = self.session.get('%s/shows.php' % self.server_url)
soup = BeautifulSoup(r.content, self.required_features)
for elem in soup.find_all('h3'):
show_name = elem.a.text.lower()
show_id = int(match('show/([0-9]+)', elem.a['href']))
# we could just return the id of the queried show, but as we
# already downloaded the whole page we might as well fill in the
# information for all the shows
self.cache_for(self.get_likely_series_id, args=(show_name,), result=show_id)
return self.cached_value(self.get_likely_series_id, args=(name,))
@cachedmethod
def get_episode_url(self, series_id, season, number):
"""Get the Addic7ed id for the given episode. Raises KeyError if none
could be found
"""
# download the page of the show, contains ids for all episodes all seasons
r = self.session.get('%s/show/%d' % (self.server_url, series_id))
soup = BeautifulSoup(r.content, self.required_features)
form = soup.find('form', attrs={'name': 'multidl'})
for table in form.find_all('table'):
for row in table.find_all('tr'):
cell = row.find('td', 'MultiDldS')
if not cell:
continue
m = matches('/serie/.+/([0-9]+)/([0-9]+)/', cell.a['href'])
if not m:
continue
episode_url = cell.a['href']
season_number = int(m[0])
episode_number = int(m[1])
# we could just return the url of the queried episode, but as we
# already downloaded the whole page we might as well fill in the
# information for all the episodes of the show
self.cache_for(self.get_episode_url, args=(series_id, season_number, episode_number), result=episode_url)
# raises KeyError if not found
return self.cached_value(self.get_episode_url, args=(series_id, season, number))
# Do not cache this method in order to always check for the most recent
# subtitles
def get_sub_urls(self, episode_url):
suburls = []
r = self.session.get('%s/%s' % (self.server_url, episode_url))
epsoup = BeautifulSoup(r.content, self.required_features)
for releaseTable in epsoup.find_all('table', 'tabel95'):
releaseRow = releaseTable.find('td', 'NewsTitle')
if not releaseRow:
continue
release = releaseRow.text.strip()
for row in releaseTable.find_all('tr'):
link = row.find('a', 'buttonDownload')
if not link:
continue
if 'href' not in link.attrs or not (link['href'].startswith('/original') or link['href'].startswith('/updated')):
continue
suburl = link['href']
lang = self.get_language(row.find('td', 'language').text.strip())
result = {'suburl': suburl, 'language': lang, 'release': release}
suburls.append(result)
return suburls
def list_checked(self, video, languages):
return self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode)
def query(self, filepath, languages, keywords, series, season, episode):
logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
self.init_cache()
try:
sid = self.get_likely_series_id(series.lower())
except KeyError:
logger.debug(u'Could not find series id for %s' % series)
return []
try:
ep_url = self.get_episode_url(sid, season, episode)
except KeyError:
logger.debug(u'Could not find episode id for %s season %d episode %d' % (series, season, episode))
return []
suburls = self.get_sub_urls(ep_url)
# filter the subtitles with our queried languages
subtitles = []
for suburl in suburls:
language = suburl['language']
if language not in languages:
continue
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(),
'%s/%s' % (self.server_url, suburl['suburl']),
keywords=[suburl['release']])
subtitles.append(subtitle)
return subtitles
Service = Addic7ed
+99
View File
@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..cache import cachedmethod
from ..exceptions import ServiceError
from ..language import language_set
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..utils import to_unicode
from ..videos import Episode
from bs4 import BeautifulSoup
import logging
import urllib
try:
import cPickle as pickle
except ImportError:
import pickle
logger = logging.getLogger(__name__)
class BierDopje(ServiceBase):
server_url = 'http://api.bierdopje.com/A2B638AC5D804C2E/'
api_based = True
languages = language_set(['eng', 'dut'])
videos = [Episode]
require_video = False
required_features = ['xml']
@cachedmethod
def get_show_id(self, series):
r = self.session.get('%sGetShowByName/%s' % (self.server_url, urllib.quote(series.lower())))
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return None
soup = BeautifulSoup(r.content, self.required_features)
if soup.status.contents[0] == 'false':
logger.debug(u'Could not find show %s' % series)
return None
return int(soup.showid.contents[0])
def load_cache(self):
logger.debug(u'Loading showids from cache...')
with self.lock:
with open(self.showids_cache, 'r') as f:
self.showids = pickle.load(f)
def query(self, filepath, season, episode, languages, tvdbid=None, series=None):
self.init_cache()
if series:
request_id = self.get_show_id(series.lower())
if request_id is None:
return []
request_source = 'showid'
request_is_tvdbid = 'false'
elif tvdbid:
request_id = tvdbid
request_source = 'tvdbid'
request_is_tvdbid = 'true'
else:
raise ServiceError('One or more parameter missing')
subtitles = []
for language in languages:
logger.debug(u'Getting subtitles for %s %d season %d episode %d with language %s' % (request_source, request_id, season, episode, language.alpha2))
r = self.session.get('%sGetAllSubsFor/%s/%s/%s/%s/%s' % (self.server_url, request_id, season, episode, language.alpha2, request_is_tvdbid))
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
soup = BeautifulSoup(r.content, self.required_features)
if soup.status.contents[0] == 'false':
logger.debug(u'Could not find subtitles for %s %d season %d episode %d with language %s' % (request_source, request_id, season, episode, language.alpha2))
continue
path = get_subtitle_path(filepath, language, self.config.multi)
for result in soup.results('result'):
subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link=result.downloadlink.contents[0],
release=to_unicode(result.filename.contents[0]))
subtitles.append(subtitle)
return subtitles
def list_checked(self, video, languages):
return self.query(video.path or video.release, video.season, video.episode, languages, video.tvdbid, video.series)
Service = BierDopje
+158
View File
@@ -0,0 +1,158 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..exceptions import ServiceError, DownloadFailedError
from ..language import Language, language_set
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..utils import to_unicode
from ..videos import Episode, Movie
import gzip
import logging
import os.path
import xmlrpclib
logger = logging.getLogger(__name__)
class OpenSubtitles(ServiceBase):
server_url = 'http://api.opensubtitles.org/xml-rpc'
api_based = True
# Source: http://www.opensubtitles.org/addons/export_languages.php
languages = language_set(['aar', 'abk', 'ace', 'ach', 'ada', 'ady', 'afa', 'afh', 'afr', 'ain', 'aka', 'akk',
'alb', 'ale', 'alg', 'alt', 'amh', 'ang', 'apa', 'ara', 'arc', 'arg', 'arm', 'arn',
'arp', 'art', 'arw', 'asm', 'ast', 'ath', 'aus', 'ava', 'ave', 'awa', 'aym', 'aze',
'bad', 'bai', 'bak', 'bal', 'bam', 'ban', 'baq', 'bas', 'bat', 'bej', 'bel', 'bem',
'ben', 'ber', 'bho', 'bih', 'bik', 'bin', 'bis', 'bla', 'bnt', 'bos', 'bra', 'bre',
'btk', 'bua', 'bug', 'bul', 'bur', 'byn', 'cad', 'cai', 'car', 'cat', 'cau', 'ceb',
'cel', 'cha', 'chb', 'che', 'chg', 'chi', 'chk', 'chm', 'chn', 'cho', 'chp', 'chr',
'chu', 'chv', 'chy', 'cmc', 'cop', 'cor', 'cos', 'cpe', 'cpf', 'cpp', 'cre', 'crh',
'crp', 'csb', 'cus', 'cze', 'dak', 'dan', 'dar', 'day', 'del', 'den', 'dgr', 'din',
'div', 'doi', 'dra', 'dua', 'dum', 'dut', 'dyu', 'dzo', 'efi', 'egy', 'eka', 'ell',
'elx', 'eng', 'enm', 'epo', 'est', 'ewe', 'ewo', 'fan', 'fao', 'fat', 'fij', 'fil',
'fin', 'fiu', 'fon', 'fre', 'frm', 'fro', 'fry', 'ful', 'fur', 'gaa', 'gay', 'gba',
'gem', 'geo', 'ger', 'gez', 'gil', 'gla', 'gle', 'glg', 'glv', 'gmh', 'goh', 'gon',
'gor', 'got', 'grb', 'grc', 'grn', 'guj', 'gwi', 'hai', 'hat', 'hau', 'haw', 'heb',
'her', 'hil', 'him', 'hin', 'hit', 'hmn', 'hmo', 'hrv', 'hun', 'hup', 'iba', 'ibo',
'ice', 'ido', 'iii', 'ijo', 'iku', 'ile', 'ilo', 'ina', 'inc', 'ind', 'ine', 'inh',
'ipk', 'ira', 'iro', 'ita', 'jav', 'jpn', 'jpr', 'jrb', 'kaa', 'kab', 'kac', 'kal',
'kam', 'kan', 'kar', 'kas', 'kau', 'kaw', 'kaz', 'kbd', 'kha', 'khi', 'khm', 'kho',
'kik', 'kin', 'kir', 'kmb', 'kok', 'kom', 'kon', 'kor', 'kos', 'kpe', 'krc', 'kro',
'kru', 'kua', 'kum', 'kur', 'kut', 'lad', 'lah', 'lam', 'lao', 'lat', 'lav', 'lez',
'lim', 'lin', 'lit', 'lol', 'loz', 'ltz', 'lua', 'lub', 'lug', 'lui', 'lun', 'luo',
'lus', 'mac', 'mad', 'mag', 'mah', 'mai', 'mak', 'mal', 'man', 'mao', 'map', 'mar',
'mas', 'may', 'mdf', 'mdr', 'men', 'mga', 'mic', 'min', 'mkh', 'mlg', 'mlt', 'mnc',
'mni', 'mno', 'moh', 'mon', 'mos', 'mun', 'mus', 'mwl', 'mwr', 'myn', 'myv', 'nah',
'nai', 'nap', 'nau', 'nav', 'nbl', 'nde', 'ndo', 'nds', 'nep', 'new', 'nia', 'nic',
'niu', 'nno', 'nob', 'nog', 'non', 'nor', 'nso', 'nub', 'nwc', 'nya', 'nym', 'nyn',
'nyo', 'nzi', 'oci', 'oji', 'ori', 'orm', 'osa', 'oss', 'ota', 'oto', 'paa', 'pag',
'pal', 'pam', 'pan', 'pap', 'pau', 'peo', 'per', 'phi', 'phn', 'pli', 'pol', 'pon',
'por', 'pra', 'pro', 'pus', 'que', 'raj', 'rap', 'rar', 'roa', 'roh', 'rom', 'rum',
'run', 'rup', 'rus', 'sad', 'sag', 'sah', 'sai', 'sal', 'sam', 'san', 'sas', 'sat',
'scn', 'sco', 'sel', 'sem', 'sga', 'sgn', 'shn', 'sid', 'sin', 'sio', 'sit', 'sla',
'slo', 'slv', 'sma', 'sme', 'smi', 'smj', 'smn', 'smo', 'sms', 'sna', 'snd', 'snk',
'sog', 'som', 'son', 'sot', 'spa', 'srd', 'srp', 'srr', 'ssa', 'ssw', 'suk', 'sun',
'sus', 'sux', 'swa', 'swe', 'syr', 'tah', 'tai', 'tam', 'tat', 'tel', 'tem', 'ter',
'tet', 'tgk', 'tgl', 'tha', 'tib', 'tig', 'tir', 'tiv', 'tkl', 'tlh', 'tli', 'tmh',
'tog', 'ton', 'tpi', 'tsi', 'tsn', 'tso', 'tuk', 'tum', 'tup', 'tur', 'tut', 'tvl',
'twi', 'tyv', 'udm', 'uga', 'uig', 'ukr', 'umb', 'urd', 'uzb', 'vai', 'ven', 'vie',
'vol', 'vot', 'wak', 'wal', 'war', 'was', 'wel', 'wen', 'wln', 'wol', 'xal', 'xho',
'yao', 'yap', 'yid', 'yor', 'ypk', 'zap', 'zen', 'zha', 'znd', 'zul', 'zun',
'por-BR', 'rum-MD'])
language_map = {'mol': Language('rum-MD'), 'scc': Language('srp'), 'pob': Language('por-BR'),
Language('rum-MD'): 'mol', Language('srp'): 'scc', Language('por-BR'): 'pob'}
language_code = 'alpha3'
videos = [Episode, Movie]
require_video = False
confidence_order = ['moviehash', 'imdbid', 'fulltext']
def __init__(self, config=None):
super(OpenSubtitles, self).__init__(config)
self.server = xmlrpclib.ServerProxy(self.server_url)
self.token = None
def init(self):
super(OpenSubtitles, self).init()
result = self.server.LogIn('', '', 'eng', self.user_agent)
if result['status'] != '200 OK':
raise ServiceError('Login failed')
self.token = result['token']
def terminate(self):
super(OpenSubtitles, self).terminate()
if self.token:
self.server.LogOut(self.token)
def query(self, filepath, languages, moviehash=None, size=None, imdbid=None, query=None):
searches = []
if moviehash and size:
searches.append({'moviehash': moviehash, 'moviebytesize': size})
if imdbid:
searches.append({'imdbid': imdbid})
if query:
searches.append({'query': query})
if not searches:
raise ServiceError('One or more parameter missing')
for search in searches:
search['sublanguageid'] = ','.join(self.get_code(l) for l in languages)
logger.debug(u'Getting subtitles %r with token %s' % (searches, self.token))
results = self.server.SearchSubtitles(self.token, searches)
if not results['data']:
logger.debug(u'Could not find subtitles for %r with token %s' % (searches, self.token))
return []
subtitles = []
for result in results['data']:
language = self.get_language(result['SubLanguageID'])
path = get_subtitle_path(filepath, language, self.config.multi)
confidence = 1 - float(self.confidence_order.index(result['MatchedBy'])) / float(len(self.confidence_order))
subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link=result['SubDownloadLink'],
release=to_unicode(result['SubFileName']), confidence=confidence)
subtitles.append(subtitle)
return subtitles
def list_checked(self, video, languages):
results = []
if video.exists:
results = self.query(video.path or video.release, languages, moviehash=video.hashes['OpenSubtitles'], size=str(video.size))
elif video.imdbid:
results = self.query(video.path or video.release, languages, imdbid=video.imdbid)
elif isinstance(video, Episode):
results = self.query(video.path or video.release, languages, query=video.series)
elif isinstance(video, Movie):
results = self.query(video.path or video.release, languages, query=video.title)
return results
def download(self, subtitle):
#TODO: Use OpenSubtitles DownloadSubtitles method
try:
self.download_file(subtitle.link, subtitle.path + '.gz')
with open(subtitle.path, 'wb') as dump:
gz = gzip.open(subtitle.path + '.gz')
dump.write(gz.read())
gz.close()
except Exception as e:
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
raise DownloadFailedError(str(e))
finally:
if os.path.exists(subtitle.path + '.gz'):
os.remove(subtitle.path + '.gz')
return subtitle
Service = OpenSubtitles
+110
View File
@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..exceptions import ServiceError, DownloadFailedError
from ..language import language_set, Language
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..utils import to_unicode
from ..videos import Episode, Movie
from hashlib import md5, sha256
import logging
import xmlrpclib
logger = logging.getLogger(__name__)
class Podnapisi(ServiceBase):
server_url = 'http://ssp.podnapisi.net:8000'
api_based = True
languages = language_set(['ar', 'be', 'bg', 'bs', 'ca', 'ca', 'cs', 'da', 'de', 'el', 'en',
'es', 'et', 'fa', 'fi', 'fr', 'ga', 'he', 'hi', 'hr', 'hu', 'id',
'is', 'it', 'ja', 'ko', 'lt', 'lv', 'mk', 'ms', 'nl', 'nn', 'pl',
'pt', 'ro', 'ru', 'sk', 'sl', 'sq', 'sr', 'sv', 'th', 'tr', 'uk',
'vi', 'zh', 'es-ar', 'pt-br'])
language_map = {'jp': Language('jpn'), Language('jpn'): 'jp',
'gr': Language('gre'), Language('gre'): 'gr',
'pb': Language('por-BR'), Language('por-BR'): 'pb',
'ag': Language('spa-AR'), Language('spa-AR'): 'ag',
'cyr': Language('srp')}
videos = [Episode, Movie]
require_video = True
def __init__(self, config=None):
super(Podnapisi, self).__init__(config)
self.server = xmlrpclib.ServerProxy(self.server_url)
self.token = None
def init(self):
super(Podnapisi, self).init()
result = self.server.initiate(self.user_agent)
if result['status'] != 200:
raise ServiceError('Initiate failed')
username = 'python_subliminal'
password = sha256(md5('XWFXQ6gE5Oe12rv4qxXX').hexdigest() + result['nonce']).hexdigest()
self.token = result['session']
result = self.server.authenticate(self.token, username, password)
if result['status'] != 200:
raise ServiceError('Authenticate failed')
def terminate(self):
super(Podnapisi, self).terminate()
def query(self, filepath, languages, moviehash):
results = self.server.search(self.token, [moviehash])
if results['status'] != 200:
logger.error('Search failed with error code %d' % results['status'])
return []
if not results['results'] or not results['results'][moviehash]['subtitles']:
logger.debug(u'Could not find subtitles for %r with token %s' % (moviehash, self.token))
return []
subtitles = []
for result in results['results'][moviehash]['subtitles']:
language = self.get_language(result['lang'])
if language not in languages:
continue
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link=result['id'],
release=to_unicode(result['release']), confidence=result['weight'])
subtitles.append(subtitle)
if not subtitles:
return []
# Convert weight to confidence
max_weight = float(max([s.confidence for s in subtitles]))
min_weight = float(min([s.confidence for s in subtitles]))
for subtitle in subtitles:
if max_weight == 0 and min_weight == 0:
subtitle.confidence = 1.0
else:
subtitle.confidence = (subtitle.confidence - min_weight) / (max_weight - min_weight)
return subtitles
def list_checked(self, video, languages):
results = self.query(video.path, languages, video.hashes['OpenSubtitles'])
return results
def download(self, subtitle):
results = self.server.download(self.token, [subtitle.link])
if results['status'] != 200:
raise DownloadFailedError()
subtitle.link = 'http://www.podnapisi.net/static/podnapisi/' + results['names'][0]['filename']
self.download_file(subtitle.link, subtitle.path)
return subtitle
Service = Podnapisi
+101
View File
@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..exceptions import ServiceError
from ..language import language_set, Language
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode, Movie
from bs4 import BeautifulSoup
from subliminal.utils import get_keywords, split_keyword
import logging
import re
import urllib
logger = logging.getLogger(__name__)
class SubsWiki(ServiceBase):
server_url = 'http://www.subswiki.com'
api_based = False
languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'por-BR', 'por', 'spa-ES', u'spa', u'ita', u'cat'])
language_map = {u'Español': Language('spa'), u'Español (España)': Language('spa'), u'Español (Latinoamérica)': Language('spa'),
u'Català': Language('cat'), u'Brazilian': Language('por-BR'), u'English (US)': Language('eng-US'),
u'English (UK)': Language('eng-GB')}
language_code = 'name'
videos = [Episode, Movie]
require_video = False
release_pattern = re.compile('\nVersion (.+), ([0-9]+).([0-9])+ MBs')
required_features = ['permissive']
def list_checked(self, video, languages):
results = []
if isinstance(video, Episode):
results = self.query(video.path or video.release, languages, get_keywords(video.guess), series=video.series, season=video.season, episode=video.episode)
elif isinstance(video, Movie) and video.year:
results = self.query(video.path or video.release, languages, get_keywords(video.guess), movie=video.title, year=video.year)
return results
def query(self, filepath, languages, keywords=None, series=None, season=None, episode=None, movie=None, year=None):
if series and season and episode:
request_series = series.lower().replace(' ', '_')
if isinstance(request_series, unicode):
request_series = request_series.encode('utf-8')
logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
r = self.session.get('%s/serie/%s/%s/%s/' % (self.server_url, urllib.quote(request_series), season, episode))
if r.status_code == 404:
logger.debug(u'Could not find subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
return []
elif movie and year:
request_movie = movie.title().replace(' ', '_')
if isinstance(request_movie, unicode):
request_movie = request_movie.encode('utf-8')
logger.debug(u'Getting subtitles for %s (%d) with languages %r' % (movie, year, languages))
r = self.session.get('%s/film/%s_(%d)' % (self.server_url, urllib.quote(request_movie), year))
if r.status_code == 404:
logger.debug(u'Could not find subtitles for %s (%d) with languages %r' % (movie, year, languages))
return []
else:
raise ServiceError('One or more parameter missing')
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
soup = BeautifulSoup(r.content, self.required_features)
subtitles = []
for sub in soup('td', {'class': 'NewsTitle'}):
sub_keywords = split_keyword(self.release_pattern.search(sub.contents[1]).group(1).lower())
if not keywords & sub_keywords:
logger.debug(u'None of subtitle keywords %r in %r' % (sub_keywords, keywords))
continue
for html_language in sub.parent.parent.findAll('td', {'class': 'language'}):
language = self.get_language(html_language.string.strip())
if language not in languages:
logger.debug(u'Language %r not in wanted languages %r' % (language, languages))
continue
html_status = html_language.findNextSibling('td')
status = html_status.find('strong').string.strip()
if status != 'Completed':
logger.debug(u'Wrong subtitle status %s' % status)
continue
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link='%s%s' % (self.server_url, html_status.findNext('td').find('a')['href']))
subtitles.append(subtitle)
return subtitles
Service = SubsWiki
+87
View File
@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..language import language_set, Language
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode
from bs4 import BeautifulSoup
from subliminal.utils import get_keywords, split_keyword
import logging
import re
import unicodedata
import urllib
logger = logging.getLogger(__name__)
class Subtitulos(ServiceBase):
server_url = 'http://www.subtitulos.es'
api_based = False
languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'por-BR', 'por', 'spa-ES', u'spa', u'ita', u'cat'])
language_map = {u'Español': Language('spa'), u'Español (España)': Language('spa'), u'Español (Latinoamérica)': Language('spa'),
u'Català': Language('cat'), u'Brazilian': Language('por-BR'), u'English (US)': Language('eng-US'),
u'English (UK)': Language('eng-GB')}
language_code = 'name'
videos = [Episode]
require_video = False
required_features = ['permissive']
# the '.+' in the pattern for Version allows us to match both '&oacute;'
# and the 'ó' char directly. This is because now BS4 converts the html
# code chars into their equivalent unicode char
release_pattern = re.compile('Versi.+n (.+) ([0-9]+).([0-9])+ megabytes')
def list_checked(self, video, languages):
return self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode)
def query(self, filepath, languages, keywords, series, season, episode):
request_series = series.lower().replace(' ', '_')
if isinstance(request_series, unicode):
request_series = unicodedata.normalize('NFKD', request_series).encode('ascii', 'ignore')
logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
r = self.session.get('%s/%s/%sx%.2d' % (self.server_url, urllib.quote(request_series), season, episode))
if r.status_code == 404:
logger.debug(u'Could not find subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
return []
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
soup = BeautifulSoup(r.content, self.required_features)
subtitles = []
for sub in soup('div', {'id': 'version'}):
sub_keywords = split_keyword(self.release_pattern.search(sub.find('p', {'class': 'title-sub'}).contents[1]).group(1).lower())
if not keywords & sub_keywords:
logger.debug(u'None of subtitle keywords %r in %r' % (sub_keywords, keywords))
continue
for html_language in sub.findAllNext('ul', {'class': 'sslist'}):
language = self.get_language(html_language.findNext('li', {'class': 'li-idioma'}).find('strong').contents[0].string.strip())
if language not in languages:
logger.debug(u'Language %r not in wanted languages %r' % (language, languages))
continue
html_status = html_language.findNext('li', {'class': 'li-estado green'})
status = html_status.contents[0].string.strip()
if status != 'Completado':
logger.debug(u'Wrong subtitle status %s' % status)
continue
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link=html_status.findNext('span', {'class': 'descargar green'}).find('a')['href'], keywords=sub_keywords)
subtitles.append(subtitle)
return subtitles
Service = Subtitulos
+63
View File
@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..language import language_set
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode, Movie, UnknownVideo
import logging
logger = logging.getLogger(__name__)
class TheSubDB(ServiceBase):
server_url = 'http://api.thesubdb.com'
user_agent = 'SubDB/1.0 (subliminal/0.6; https://github.com/Diaoul/subliminal)'
api_based = True
# Source: http://api.thesubdb.com/?action=languages
languages = language_set(['af', 'cs', 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'id', 'it',
'la', 'nl', 'no', 'oc', 'pl', 'pt', 'ro', 'ru', 'sl', 'sr', 'sv',
'tr'])
videos = [Movie, Episode, UnknownVideo]
require_video = True
def list_checked(self, video, languages):
return self.query(video.path, video.hashes['TheSubDB'], languages)
def query(self, filepath, moviehash, languages):
r = self.session.get(self.server_url, params={'action': 'search', 'hash': moviehash})
if r.status_code == 404:
logger.debug(u'Could not find subtitles for hash %s' % moviehash)
return []
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
available_languages = language_set(r.content.split(','))
languages &= available_languages
if not languages:
logger.debug(u'Could not find subtitles for hash %s with languages %r (only %r available)' % (moviehash, languages, available_languages))
return []
subtitles = []
for language in languages:
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), '%s?action=download&hash=%s&language=%s' % (self.server_url, moviehash, language.alpha2))
subtitles.append(subtitle)
return subtitles
Service = TheSubDB
+141
View File
@@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
# Copyright 2012 Nicolas Wack <wackou@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..cache import cachedmethod
from ..language import language_set, Language
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..utils import get_keywords
from ..videos import Episode
from bs4 import BeautifulSoup
import logging
import re
logger = logging.getLogger(__name__)
def match(pattern, string):
try:
return re.search(pattern, string).group(1)
except AttributeError:
logger.debug(u'Could not match %r on %r' % (pattern, string))
return None
class TvSubtitles(ServiceBase):
server_url = 'http://www.tvsubtitles.net'
api_based = False
languages = language_set(['ar', 'bg', 'cs', 'da', 'de', 'el', 'en', 'es', 'fi', 'fr', 'hu',
'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ro', 'ru', 'sv', 'tr', 'uk',
'zh', 'pt-br'])
#TODO: Find more exceptions
language_map = {'gr': Language('gre'), 'cz': Language('cze'), 'ua': Language('ukr')}
videos = [Episode]
require_video = False
required_features = ['permissive']
@cachedmethod
def get_likely_series_id(self, name):
r = self.session.post('%s/search.php' % self.server_url, data={'q': name})
soup = BeautifulSoup(r.content, self.required_features)
maindiv = soup.find('div', 'left')
results = []
for elem in maindiv.find_all('li'):
sid = int(match('tvshow-([0-9]+)\.html', elem.a['href']))
show_name = match('(.*) \(', elem.a.text)
results.append((show_name, sid))
#TODO: pick up the best one in a smart way
result = results[0]
return result[1]
@cachedmethod
def get_episode_id(self, series_id, season, number):
"""Get the TvSubtitles id for the given episode. Raises KeyError if none
could be found."""
# download the page of the season, contains ids for all episodes
episode_id = None
r = self.session.get('%s/tvshow-%d-%d.html' % (self.server_url, series_id, season))
soup = BeautifulSoup(r.content, self.required_features)
table = soup.find('table', id='table5')
for row in table.find_all('tr'):
cells = row.find_all('td')
if not cells:
continue
episode_number = match('x([0-9]+)', cells[0].text)
if not episode_number:
continue
episode_number = int(episode_number)
episode_id = int(match('episode-([0-9]+)', cells[1].a['href']))
# we could just return the id of the queried episode, but as we
# already downloaded the whole page we might as well fill in the
# information for all the episodes of the season
self.cache_for(self.get_episode_id, args=(series_id, season, episode_number), result=episode_id)
# raises KeyError if not found
return self.cached_value(self.get_episode_id, args=(series_id, season, number))
# Do not cache this method in order to always check for the most recent
# subtitles
def get_sub_ids(self, episode_id):
subids = []
r = self.session.get('%s/episode-%d.html' % (self.server_url, episode_id))
epsoup = BeautifulSoup(r.content, self.required_features)
for subdiv in epsoup.find_all('a'):
if 'href' not in subdiv.attrs or not subdiv['href'].startswith('/subtitle'):
continue
subid = int(match('([0-9]+)', subdiv['href']))
lang = self.get_language(match('flags/(.*).gif', subdiv.img['src']))
result = {'subid': subid, 'language': lang}
for p in subdiv.find_all('p'):
if 'alt' in p.attrs and p['alt'] == 'rip':
result['rip'] = p.text.strip()
if 'alt' in p.attrs and p['alt'] == 'release':
result['release'] = p.text.strip()
subids.append(result)
return subids
def list_checked(self, video, languages):
return self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode)
def query(self, filepath, languages, keywords, series, season, episode):
logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
self.init_cache()
sid = self.get_likely_series_id(series.lower())
try:
ep_id = self.get_episode_id(sid, season, episode)
except KeyError:
logger.debug(u'Could not find episode id for %s season %d episode %d' % (series, season, episode))
return []
subids = self.get_sub_ids(ep_id)
# filter the subtitles with our queried languages
subtitles = []
for subid in subids:
language = subid['language']
if language not in languages:
continue
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(),
'%s/download-%d.html' % (self.server_url, subid['subid']),
keywords=[subid['rip'], subid['release']])
subtitles.append(subtitle)
return subtitles
def download(self, subtitle):
self.download_zip_file(subtitle.link, subtitle.path)
Service = TvSubtitles
-427
View File
@@ -1,427 +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/>.
#
from itertools import groupby
import ConfigParser
import PluginWorker
import Queue
import locale
import logging
import mimetypes
import os
import plugins
import sys
import traceback
import locale
import encodingKludge as ek
SUPPORTED_FORMATS = 'video/x-msvideo', 'video/quicktime', 'video/x-matroska', 'video/mp4'
logger = logging.getLogger('subliminal')
SYS_ENCODING = None
try:
locale.setlocale(locale.LC_ALL, "")
SYS_ENCODING = locale.getpreferredencoding()
except (locale.Error, IOError):
pass
# for OSes that are poorly configured I'll just force UTF-8
if not SYS_ENCODING or SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
SYS_ENCODING = 'UTF-8'
class Subliminal(object):
"""Main Subliminal class"""
def __init__(self, config=True, cache_dir=True, workers=4, multi=False, force=False, max_depth=3, autostart=False, plugins_config=None, files_mode=-1):
# set default values
self.multi = multi
self.force = force
self.max_depth = max_depth
self.config = None
self.config_file = None
self.cache_dir = None
self.taskQueue = Queue.Queue()
self.resultQueue = Queue.Queue()
self._languages = None
self._plugins = self.listAPIPlugins()
self.workers = workers
self.plugins_config = plugins_config
self.files_mode = files_mode
if autostart:
self.startWorkers()
# handle configuration file preferences
try:
if config == True: # default configuration file
import xdg.BaseDirectory as bd
self.config = ConfigParser.SafeConfigParser({"languages": "", "plugins": ""})
self.config_file = ek.ek(os.path.join, bd.xdg_config_home, "subliminal", "config.ini")
if not ek.ek(os.path.exists, self.config_file): # configuration file doesn't exist, create it
self._createConfigFile()
else: # configuration file exists, load it
self._loadConfigFile()
elif config: # custom configuration file
self.config = ConfigParser.SafeConfigParser({"languages": "", "plugins": ""})
self.config_file = config
if not ek.ek(os.path.isfile, self.config_file): # custom configuration file doesn't exist, create it
self._createConfigFile()
else:
self._loadConfigFile()
except:
self.config = None
self.config_file = None
logger.error(u"Failed to use the configuration file, continue without it")
raise
# handle cache directory preferences
try:
if cache_dir == True: # default cache directory
import xdg.BaseDirectory as bd
self.cache_dir = ek.ek(os.path.join, bd.xdg_config_home, "subliminal", "cache")
if not ek.ek(os.path.exists, self.cache_dir): # cache directory doesn't exist, create it
ek.ek(os.mkdir, self.cache_dir)
logger.debug(u'Creating cache directory: %s' % self.cache_dir)
elif cache_dir: # custom configuration file
self.cache_dir = cache_dir
if not ek.ek(os.path.isdir, self.cache_dir): # custom v file doesn't exist, create it
ek.ek(os.mkdir, self.cache_dir)
logger.debug(u'Creating cache directory: %s' % self.cache_dir)
except:
self.cache_dir = None
logger.error(u"Failed to use the cache directory, continue without it")
def _loadConfigFile(self):
"""Load a configuration file specified in self.config_file"""
self.config.read(self.config_file)
self._loadLanguagesFromConfig()
self._loadPluginsFromConfig()
def _createConfigFile(self):
"""Create a configuration file specified in self.config_file"""
folder = ek.ek(os.path.dirname, self.config_file)
if not ek.ek(os.path.exists, folder):
logger.info(u"Creating folder: %s" % folder)
ek.ek(os.mkdir, folder)
# try to load a language from system
self._loadLanguageFromSystem()
self.config.set("DEFAULT", "languages", ",".join(self._languages))
self.config.set("DEFAULT", "plugins", ",".join(self._plugins))
self.config.add_section("SubtitleSource")
self.config.set("SubtitleSource", "key", "")
self._writeConfigFile()
logger.info(u"Creating configuration file: %s" % self.config_file)
logger.debug(u"Languages in created configuration file: %s" % self._languages)
logger.debug(u"Plugins in created configuration file: %s" % self._plugins)
@staticmethod
def listExistingPlugins():
"""List all possible plugins"""
return map(lambda x: x.__name__, plugins.PluginBase.PluginBase.__subclasses__())
@staticmethod
def listAPIPlugins():
"""List plugins that use API"""
return filter(Subliminal.isAPIBasedPlugin, Subliminal.listExistingPlugins())
def _writeConfigFile(self):
"""Write the configuration file"""
configfile = open(self.config_file, "w")
self.config.write(configfile)
configfile.close()
def get_languages(self):
"""Get current languages"""
return self._languages
def set_languages(self, value):
"""Set languages and save to configuration file if specified by the constructor"""
logger.debug(u"Setting languages to %s" % value)
self._languages = value
if self.config:
self._saveLanguagesToConfig()
@staticmethod
def isValidLanguage(language):
"""Check if a language is valid"""
if len(language) != 2:
logger.error(u"Language %s is not valid" % language)
return False
return True
def _saveLanguagesToConfig(self):
"""Save languages to configuration file"""
logger.debug(u"Saving languages %s to configuration file" % self._languages)
self.config.set("DEFAULT", "languages", ",".join(self._languages))
self._writeConfigFile()
def _loadLanguagesFromConfig(self):
"""Load languages from configuration file"""
configLanguages = self.config.get("DEFAULT", "languages")
logger.debug(u"Loading languages %s from configuration file" % configLanguages)
if not configLanguages:
self._languages = None
return
self._languages = filter(self.isValidLanguage, map(str.strip, configLanguages.split(",")))
def _loadLanguageFromSystem(self):
"""Load language from system"""
logger.debug(u"Loading language from system")
try:
self._languages = [locale.getdefaultlocale()[0][:2]]
logger.debug(u"Language %s loaded from system" % self._languages)
except:
logger.warning(u"Could not read language from system")
def get_plugins(self):
"""Get current plugins"""
return self._plugins
def set_plugins(self, value):
"""Set plugins and save to configuration file if specified by the constructor"""
logger.debug(u'Setting plugins to %r' % value)
self._plugins = filter(self.isValidPlugin, value)
if self.config:
self._savePluginsToConfig()
@staticmethod
def isValidPlugin(pluginName):
"""Check if a plugin is valid (exists)"""
if pluginName not in Subliminal.listExistingPlugins():
logger.error(u"Plugin %s does not exist" % pluginName)
return False
return True
@staticmethod
def isAPIBasedPlugin(pluginName):
"""Check if a plugin is API-based"""
if not getattr(plugins, pluginName).api_based:
return False
return True
def _savePluginsToConfig(self):
"""Save plugins to configuration file"""
logger.debug(u"Saving plugins %s to configuration file" % self._plugins)
self.config.set("DEFAULT", "plugins", ",".join(self._plugins))
self._writeConfigFile()
def _loadPluginsFromConfig(self):
"""Load plugins from configuration file"""
configPlugins = self.config.get("DEFAULT", "plugins")
logger.debug(u"Loading plugins %s from configuration file" % configPlugins)
self._plugins = filter(self.isValidPlugin, map(str.strip, configPlugins.split(",")))
# getters/setters for the property _languages and _plugins
languages = property(get_languages, set_languages)
plugins = property(get_plugins, set_plugins)
def deactivatePlugin(self, plugin):
"""Deactivate a plugin"""
self._plugins.remove(plugin)
if self.config:
self._savePluginsToConfig()
def activatePlugin(self, plugin):
"""Activate a plugin"""
if self.isValidPlugin(plugin):
self._plugins.append(plugin)
if self.config:
self._savePluginsToConfig()
def listSubtitles(self, entries):
"""
Searches subtitles within the active plugins and returns all found matching subtitles.
entries can be:
- filepaths
- folderpaths (N.B. internal recursive search function will be used)
- filenames
"""
search_results = []
if isinstance(entries, basestring):
entries = [ek.fixStupidEncodings(entries)]
elif not isinstance(entries, list):
raise TypeError('Entries should be a list or a string')
for e in entries:
search_results.extend(self._recursiveSearch(e))
taskCount = 0
for (l, f) in search_results:
taskCount += self.searchSubtitlesThreaded(f, l)
subtitles = []
for i in range(taskCount):
subtitles.extend(self.resultQueue.get(timeout=10))
return subtitles
@staticmethod
def arrangeSubtitles(subtitles):
"""Arrange subtitles in a handy dict by filename, language and plugin"""
arrangedSubtitles = {}
for (filename, subsByFilename) in groupby(sorted(subtitles, key=lambda x: x["filename"]), lambda x: x["filename"]):
arrangedSubtitles[filename] = {}
for (language, subsByFilenameByLanguage) in groupby(sorted(subsByFilename, key=lambda x: x["lang"]), lambda x: x["lang"]):
arrangedSubtitles[filename][language] = {}
for (plugin, subsByFilenameByLanguageByPlugin) in groupby(sorted(subsByFilenameByLanguage, key=lambda x: x["plugin"]), lambda x: x["plugin"]):
arrangedSubtitles[filename][language][plugin] = sorted(list(subsByFilenameByLanguageByPlugin))
return arrangedSubtitles
def sortSubtitlesRaw(self, subtitles):
"""Sort subtitles using user defined languages and plugins"""
return sorted(subtitles, cmp=self._cmpSubtitles)
def _cmpSubtitles(self, x, y):
"""
Compares 2 subtitles elements x and y. Returns -1 if x < y, 0 if =, 1 if >
Use filename, languages and plugin comparison
"""
filenames = sorted([x['filename'], y['filename']])
if x['filename'] != y['filename'] and filenames.index(x['filename']) < filenames(y['filename']):
return - 1
if x['filename'] != y['filename'] and filenames.index(x['filename']) > filenames(y['filename']):
return 1
if self._languages and self._languages.index(x['lang']) < self._languages.index(y['lang']):
return - 1
if self._languages and self._languages.index(x['lang']) > self._languages.index(y['lang']):
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 searchSubtitlesThreaded(self, filenames, languages):
"""
Makes workers search for subtitles in different languages for multiple filenames and puts the result in the result queue.
Aslo split the work in multiple tasks
When the function returns, all the results may not be available yet!
"""
logger.info(u"Searching subtitles for %s with languages %s" % (filenames, languages))
tasks = []
for pluginName in self._plugins:
try:
plugin = getattr(plugins, pluginName)(self.getConfigDict())
except:
logger.debug(traceback.print_exc())
continue
# split tasks if the plugin can't handle multi-thing queries
tasks.extend(plugin.splitTask({'task': 'list', 'plugin': pluginName, 'languages': languages, 'filenames': filenames, 'config': self.getConfigDict()}))
for t in tasks:
self.taskQueue.put(t)
return len(tasks)
def downloadSubtitlesThreaded(self, subtitles):
"""
Makes workers download subtitles and puts the result in the result queue.
When the function returns, all the results may not be available yet!
"""
# 1 task per file if not multi, 1 task per file and per language if multi
taskCount = 0
for (filename, subsByFilename) in groupby(sorted(subtitles, key=lambda x: x["filename"]), lambda x: x["filename"]):
if not self.multi:
self.taskQueue.put({'task': 'download', 'subtitle': sorted(list(subsByFilename), cmp=self._cmpSubtitles), 'config': self.getConfigDict()})
taskCount += 1
continue
for (language, subsByFilenameByLanguage) in groupby(sorted(subsByFilename, key=lambda x: x["lang"]), lambda x: x["lang"]):
self.taskQueue.put({'task': 'download', 'subtitle': sorted(list(subsByFilenameByLanguage), cmp=self._cmpSubtitles), 'config': self.getConfigDict()})
taskCount += 1
return taskCount
def downloadSubtitles(self, entries):
"""Download subtitles recursivly in entries"""
subtitles = self.listSubtitles(entries)
taskCount = self.downloadSubtitlesThreaded(subtitles)
paths = []
for i in range(taskCount):
paths.append(self.resultQueue.get(timeout=10))
return paths
def _recursiveSearch(self, entry, depth=0):
"""
Searches files in the entry
This will output a list of tuples (filename, languages)
"""
if depth > self.max_depth and self.max_depth != 0: # we do not want to search the whole file system except if max_depth = 0
return []
if ek.ek(os.path.isfile, entry): # a file? scan it
if depth != 0: # only check for valid format if recursing, trust the user
mimetypes.add_type("video/x-matroska", ".mkv")
mimetype = mimetypes.guess_type(entry)[0]
if mimetype not in SUPPORTED_FORMATS:
return []
basepath = ek.fixStupidEncodings(ek.ek(os.path.splitext, entry)[0])
# check for .xx.srt if needed
if self.multi and self.languages:
if self.force:
return [(self.languages, [ek.ek(os.path.normpath, entry)])]
needed_languages = self.languages[:]
for l in self.languages:
if ek.ek(os.path.exists, basepath + '.%s.srt' % l):
logger.info(u"Skipping language %s for file %s as it already exists. Use the --force option to force the download" % (l, entry))
needed_languages.remove(l)
if needed_languages:
return [(needed_languages, [ek.ek(os.path.normpath, entry)])]
return []
# single subtitle download: .srt
if self.force or not ek.ek(os.path.exists, basepath + '.srt'):
return [(self.languages, [ek.ek(os.path.normpath, entry)])]
if ek.ek(os.path.isdir, entry): # a dir? recurse
#TODO if hidden folder, don't keep going (how to handle windows/mac/linux ?)
files = []
for e in ek.ek(os.listdir, entry):
files.extend(self._recursiveSearch(ek.ek(os.path.join, entry, e), depth + 1))
files.sort()
grouped_files = []
for languages, group in groupby(files, lambda t: t[0]):
filenames = []
for t in group:
filenames.extend(t[1])
grouped_files.append((languages, filenames))
return grouped_files
return [] # anything else, nothing.
def startWorkers(self):
"""Create a pool of workers and start them"""
self.pool = []
for i in range(self.workers):
worker = PluginWorker.PluginWorker(self.taskQueue, self.resultQueue)
worker.start()
self.pool.append(worker)
logger.debug(u"Worker %s added to the pool" % worker.name)
def sendStopSignal(self):
"""Send a stop signal the pool of workers (poison pill)"""
logger.debug(u"Sending %d poison pills into the task queue" % self.workers)
for i in range(self.workers):
self.taskQueue.put(None)
def stopWorkers(self):
"""Stop workers using a stop signal and wait for them to terminate properly"""
self.sendStopSignal()
for worker in self.pool:
worker.join()
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
if self.config:
config['subtitlesource_key'] = self.config.get('SubtitleSource', 'key')
if self.plugins_config and 'subtitlesource_key' in self.plugins_config:
config['subtitlesource_key'] = self.plugins_config['subtitlesource_key']
config['force'] = self.force
config['files_mode'] = self.files_mode
return config
+138
View File
@@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from .language import Language
import os.path
__all__ = ['Subtitle', 'EmbeddedSubtitle', 'ExternalSubtitle', 'ResultSubtitle', 'get_subtitle_path']
#: Subtitles extensions
EXTENSIONS = ['.srt', '.sub', '.txt']
class Subtitle(object):
"""Base class for subtitles
:param string path: path to the subtitle
:param language: language of the subtitle
:type language: :class:`~subliminal.language.Language`
"""
def __init__(self, path, language):
self.path = path
self.language = language
@property
def exists(self):
"""Whether the subtitle exists or not"""
if self.path:
return os.path.exists(self.path)
return False
class EmbeddedSubtitle(Subtitle):
"""Subtitle embedded in a container
:param string path: path to the subtitle
:param language: language of the subtitle
:type language: :class:`~subliminal.language.Language`
:param int track_id: id of the subtitle track in the container
"""
def __init__(self, path, language, track_id):
super(EmbeddedSubtitle, self).__init__(path, language)
self.track_id = track_id
@classmethod
def from_enzyme(cls, path, subtitle):
language = Language(subtitle.language) or None
return cls(path, language, subtitle.trackno)
class ExternalSubtitle(Subtitle):
"""Subtitle in a file next to the video file"""
@classmethod
def from_path(cls, path):
"""Create an :class:`ExternalSubtitle` from path"""
extension = ''
for e in EXTENSIONS:
if path.endswith(e):
extension = e
break
if not extension:
raise ValueError('Not a supported subtitle extension')
language = os.path.splitext(path[:len(path) - len(extension)])[1][1:]
language = Language(language) or None
return cls(path, language)
class ResultSubtitle(ExternalSubtitle):
"""Subtitle found using :mod:`~subliminal.services`
:param string path: path to the subtitle
:param language: language of the subtitle
:type language: :class:`~subliminal.language.Language`
:param string service: name of the service
:param string link: download link for the subtitle
:param string release: release name of the video
:param float confidence: confidence that the subtitle matches the video according to the service
:param set keywords: keywords that describe the subtitle
"""
def __init__(self, path, language, service, link, release=None, confidence=1, keywords=set()):
super(ResultSubtitle, self).__init__(path, language)
self.service = service
self.link = link
self.release = release
self.confidence = confidence
self.keywords = keywords
@property
def single(self):
"""Whether this is a single subtitle or not. A single subtitle does not have
a language indicator in its file name
:rtype: bool
"""
extension = os.path.splitext(self.path)[0]
language = os.path.splitext(self.path[:len(self.path) - len(extension)])[1][1:]
return Language(language) == Language('und')
def __repr__(self):
return 'ResultSubtitle(%s, %s, %.2f, %s)' % (self.language, self.service, self.confidence, self.release)
def get_subtitle_path(video_path, language, multi):
"""Create the subtitle path from the given video path using language if multi
:param string video_path: path to the video
:param language: language of the subtitle
:type language: :class:`~subliminal.language.Language`
:param bool multi: whether to use multi language naming or not
:return: path of the subtitle
:rtype: string
"""
if not os.path.exists(video_path):
path = os.path.splitext(os.path.basename(video_path))[0]
else:
path = os.path.splitext(video_path)[0]
if multi and language:
return path + '.%s%s' % (language.alpha2, EXTENSIONS[0])
return path + '%s' % EXTENSIONS[0]
+68
View File
@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
__all__ = ['Task', 'ListTask', 'DownloadTask', 'StopTask']
class Task(object):
"""Base class for tasks to use in subliminal"""
pass
class ListTask(Task):
"""List task used by the worker to search for subtitles
:param video: video to search subtitles for
:type video: :class:`~subliminal.videos.Video`
:param list languages: languages to search for
:param string service: name of the service to use
:param config: configuration for the service
:type config: :class:`~subliminal.services.ServiceConfig`
"""
def __init__(self, video, languages, service, config):
super(ListTask, self).__init__()
self.video = video
self.service = service
self.languages = languages
self.config = config
def __repr__(self):
return 'ListTask(%r, %r, %s, %r)' % (self.video, self.languages, self.service, self.config)
class DownloadTask(Task):
"""Download task used by the worker to download subtitles
:param video: video to download subtitles for
:type video: :class:`~subliminal.videos.Video`
:param subtitles: subtitles to download in order of preference
:type subtitles: list of :class:`~subliminal.subtitles.Subtitle`
"""
def __init__(self, video, subtitles):
super(DownloadTask, self).__init__()
self.video = video
self.subtitles = subtitles
def __repr__(self):
return 'DownloadTask(%r, %r)' % (self.video, self.subtitles)
class StopTask(Task):
"""Stop task that will stop the worker"""
pass
-206
View File
@@ -1,206 +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 unittest
import logging
import os
logging.basicConfig(level=logging.DEBUG, format='%(name)-24s %(levelname)-8s %(message)s')
if not os.path.exists('/tmp/subliminal/cache'):
os.mkdir('/tmp/subliminal/cache')
config = {'multi': True, 'cache_dir': '/tmp/subliminal/cache', 'subtitlesource_key': '', 'force': False}
class Addic7edListTestCase1(unittest.TestCase):
def runTest(self):
from subliminal.plugins import Addic7ed
plugin = Addic7ed(config)
results = plugin.list(["The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ["en", "fr"])
print results
assert len(results) > 0
class Addic7edListTestCase2(unittest.TestCase):
def runTest(self):
from subliminal.plugins import Addic7ed
plugin = Addic7ed(config)
results = plugin.list(["Dexter.S05E02.720p.HDTV.x264-IMMERSE.mkv"], ["en", "fr"])
print results
assert len(results) > 0
class Addic7edDownloadTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import Addic7ed
plugin = Addic7ed(config)
results = plugin.download(plugin.list(["/tmp/The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ["en", "fr"])[0])
print results
assert len(results) > 0
class BierDopjeListTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import BierDopje
plugin = BierDopje(config)
results = plugin.list(["The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ["en", "fr"])
print results
assert len(results) > 0
class BierDopjeListExceptionTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import BierDopje
plugin = BierDopje(config)
results = plugin.list(["The.Office.US.S07E08.Viewing.Party.HDTV.XviD-FQM.[VTV].avi"], ["en", "fr"])
print results
assert len(results) > 0
class BierDopjeListTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import BierDopje
plugin = BierDopje(config)
results = plugin.list(["The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ["en", "fr"])
print results
assert len(results) > 0
class OpenSubtitlesQueryTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import OpenSubtitles
plugin = OpenSubtitles()
results = plugin.query('Night.Watch.2004.CD1.DVDRiP.XViD-FiCO.avi', moviehash="09a2c497663259cb", bytesize="733589504") # http://trac.opensubtitles.org/projects/opensubtitles/wiki/XMLRPC
print results
assert len(results) > 0
class OpenSubtitlesQueryNoHashTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import OpenSubtitles
plugin = OpenSubtitles()
results = plugin.query('Night.Watch.2004.CD1.DVDRiP.XViD-FiCO.avi', languages=['en', 'fr']) # http://trac.opensubtitles.org/projects/opensubtitles/wiki/XMLRPC
print results
assert len(results) > 0
class OpenSubtitlesListTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import OpenSubtitles
plugin = OpenSubtitles()
results = plugin.download(plugin.query('/tmp/Night.Watch.2004.CD1.DVDRiP.XViD-FiCO.avi', moviehash="09a2c497663259cb", bytesize="733589504")[0]) # http://trac.opensubtitles.org/projects/opensubtitles/wiki/XMLRPC
assert len(results) > 0
class SubtitulosListTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import Subtitulos
plugin = Subtitulos()
results = plugin.list(["The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ['en', 'fr'])
print results
assert len(results) > 0
class SubtitulosDownloadTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import Subtitulos
plugin = Subtitulos()
results = plugin.download(plugin.list(["/tmp/The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ['en', 'fr'])[0])
print results
assert len(results) > 0
class TheSubDBQueryTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import TheSubDB
plugin = TheSubDB()
results = plugin.query("test.mkv", "edc1981d6459c6111fe36205b4aff6c2")
print results
assert len(results) > 0
class TheSubDBDownloadTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import TheSubDB
plugin = TheSubDB()
results = plugin.download(plugin.query("/tmp/test.mkv", "edc1981d6459c6111fe36205b4aff6c2")[0])
print results
assert len(results) > 0
class SubsWikiListTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import SubsWiki
plugin = SubsWiki()
results = plugin.list(["The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ['en', 'es'])
print results
assert len(results) > 0
class SubsWikiDownloadTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import SubsWiki
plugin = SubsWiki()
results = plugin.download(plugin.list(["/tmp/The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ['en', 'es'])[0])
print results
assert len(results) > 0
'''
class PodnapisiQueryTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import Podnapisi
plugin = Podnapisi()
results = plugin.query('09a2c497663259cb', ["en", "fr"])
print results
assert len(results) > 5
class SubSceneListTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import SubScene
plugin = SubScene()
results = plugin.list(["Dexter.S04E01.HDTV.XviD-NoTV.avi"], ['en', 'fr'])
print results
assert len(results) > 0, "No result could be found for Dexter.S04E01.HDTV.XviD-NoTV.avi and no languages"
class SubSceneDownloadTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import SubScene
plugin = SubScene()
results = plugin.download(plugin.list(["Dexter.S04E01.HDTV.XviD-NoTV.avi"], ['en', 'fr'])[0])
print results
assert len(results) > 0, "No result could be found for Dexter.S04E01.HDTV.XviD-NoTV.avi and no languages"
class SubtitleSourceListTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import SubtitleSource
plugin = SubtitleSource()
results = plugin.list(["PrisM-Inception.2010"], ['en', 'fr'])
print results
assert len(results) > 0, "No result could be found for PrisM-Inception.2010"
'''
if __name__ == "__main__":
unittest.main()
+64
View File
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
import re
__all__ = ['get_keywords', 'split_keyword', 'to_unicode']
def get_keywords(guess):
"""Retrieve keywords from guessed informations
:param guess: guessed informations
:type guess: :class:`guessit.guess.Guess`
:return: lower case alphanumeric keywords
:rtype: set
"""
keywords = set()
for k in ['releaseGroup', 'screenSize', 'videoCodec', 'format']:
if k in guess:
keywords = keywords | split_keyword(guess[k].lower())
return keywords
def split_keyword(keyword):
"""Split a keyword in multiple ones on any non-alphanumeric character
:param string keyword: keyword
:return: keywords
:rtype: set
"""
split = set(re.findall(r'\w+', keyword))
return split
def to_unicode(data):
"""Convert a basestring to unicode
:param basestring data: data to decode
:return: data as unicode
:rtype: unicode
"""
if not isinstance(data, basestring):
raise ValueError('Basestring expected')
if isinstance(data, unicode):
return data
return unicode(data, 'utf-8', 'replace')
-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.3'
+282
View File
@@ -0,0 +1,282 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import subtitles
import enzyme
import guessit
import hashlib
import logging
import mimetypes
import os
import struct
__all__ = ['EXTENSIONS', 'MIMETYPES', 'Video', 'Episode', 'Movie', 'UnknownVideo',
'scan', 'hash_opensubtitles', 'hash_thesubdb']
logger = logging.getLogger(__name__)
#: Video extensions
EXTENSIONS = ['.avi', '.mkv', '.mpg', '.mp4', '.m4v', '.mov', '.ogm', '.ogv', '.wmv',
'.divx', '.asf']
#: Video mimetypes
MIMETYPES = ['video/mpeg', 'video/mp4', 'video/quicktime', 'video/x-ms-wmv', 'video/x-msvideo',
'video/x-flv', 'video/x-matroska', 'video/x-matroska-3d']
class Video(object):
"""Base class for videos
:param string path: path
:param guess: guessed informations
:type guess: :class:`~guessit.guess.Guess`
:param string imdbid: imdbid
"""
def __init__(self, path, guess, imdbid=None):
self.release = path
self.guess = guess
self.imdbid = imdbid
self._path = None
self.hashes = {}
if os.path.exists(path):
self._path = path
self.size = os.path.getsize(self._path)
self._compute_hashes()
@classmethod
def from_path(cls, path):
"""Create a :class:`Video` subclass guessing all informations from the given path
:param string path: path
:return: video object
:rtype: :class:`Episode` or :class:`Movie` or :class:`UnknownVideo`
"""
guess = guessit.guess_file_info(path, 'autodetect')
result = None
if guess['type'] == 'episode' and 'series' in guess and 'season' in guess and 'episodeNumber' in guess:
title = None
if 'title' in guess:
title = guess['title']
result = Episode(path, guess['series'], guess['season'], guess['episodeNumber'], title, guess)
if guess['type'] == 'movie' and 'title' in guess:
year = None
if 'year' in guess:
year = guess['year']
result = Movie(path, guess['title'], year, guess)
if not result:
result = UnknownVideo(path, guess)
if not isinstance(result, cls):
raise ValueError('Video is not of requested type')
return result
@property
def exists(self):
"""Whether the video exists or not"""
if self._path:
return os.path.exists(self._path)
return False
@property
def path(self):
"""Path to the video"""
return self._path
@path.setter
def path(self, value):
if not os.path.exists(value):
raise ValueError('Path does not exists')
self._path = value
self.size = os.path.getsize(self._path)
self._compute_hashes()
def _compute_hashes(self):
"""Compute different hashes"""
self.hashes['OpenSubtitles'] = hash_opensubtitles(self.path)
self.hashes['TheSubDB'] = hash_thesubdb(self.path)
def scan(self):
"""Scan and return associated subtitles
:return: associated subtitles
:rtype: list of :class:`~subliminal.subtitles.Subtitle`
"""
if not self.exists:
return []
basepath = os.path.splitext(self.path)[0]
results = []
video_infos = None
try:
video_infos = enzyme.parse(self.path)
logger.debug(u'Succeeded parsing %s with enzyme: %r' % (self.path, video_infos))
except:
logger.debug(u'Failed parsing %s with enzyme' % self.path)
if isinstance(video_infos, enzyme.core.AVContainer):
results.extend([subtitles.EmbeddedSubtitle.from_enzyme(self.path, s) for s in video_infos.subtitles])
# cannot use glob here because it chokes if there are any square
# brackets inside the filename, so we have to use basic string
# startswith/endswith comparisons
folder, basename = os.path.split(basepath)
existing = [f for f in os.listdir(folder) if f.startswith(basename)]
for path in existing:
for ext in subtitles.EXTENSIONS:
if path.endswith(ext):
possible_lang = path[len(basename) + 1:-len(ext)]
if possible_lang == '':
results.append(subtitles.ExternalSubtitle(path, None))
else:
lang = guessit.Language(possible_lang)
if lang:
results.append(subtitles.ExternalSubtitle(path, lang))
return results
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.release)
def __hash__(self):
return hash(self.path or self.release)
class Episode(Video):
"""Episode :class:`Video`
:param string path: path
:param string series: series
:param int season: season number
:param int episode: episode number
:param string title: title
:param guess: guessed informations
:type guess: :class:`~guessit.guess.Guess`
:param string tvdbid: tvdbid
:param string imdbid: imdbid
"""
def __init__(self, path, series, season, episode, title=None, guess=None, tvdbid=None, imdbid=None):
super(Episode, self).__init__(path, guess, imdbid)
self.series = series
self.title = title
self.season = season
self.episode = episode
self.tvdbid = tvdbid
class Movie(Video):
"""Movie :class:`Video`
:param string path: path
:param string title: title
:param int year: year
:param guess: guessed informations
:type guess: :class:`~guessit.guess.Guess`
:param string imdbid: imdbid
"""
def __init__(self, path, title, year=None, guess=None, imdbid=None):
super(Movie, self).__init__(path, guess, imdbid)
self.title = title
self.year = year
class UnknownVideo(Video):
"""Unknown video"""
pass
def scan(entry, max_depth=3, scan_filter=None, depth=0):
"""Scan a path for videos and subtitles
:param string entry: path
:param int max_depth: maximum folder depth
:param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
:param int depth: starting depth
:return: found videos and subtitles
:rtype: list of (:class:`Video`, [:class:`~subliminal.subtitles.Subtitle`])
"""
if depth > max_depth and max_depth != 0: # we do not want to search the whole file system except if max_depth = 0
return []
if depth == 0:
entry = os.path.abspath(entry)
if os.path.isdir(entry): # a dir? recurse
logger.debug(u'Scanning directory %s with depth %d/%d' % (entry, depth, max_depth))
result = []
for e in os.listdir(entry):
result.extend(scan(os.path.join(entry, e), max_depth, scan_filter, depth + 1))
return result
if os.path.isfile(entry) or depth == 0:
logger.debug(u'Scanning file %s with depth %d/%d' % (entry, depth, max_depth))
if depth != 0: # trust the user: only check for valid format if recursing
if mimetypes.guess_type(entry)[0] not in MIMETYPES and os.path.splitext(entry)[1] not in EXTENSIONS:
return []
if scan_filter is not None and scan_filter(entry):
return []
video = Video.from_path(entry)
return [(video, video.scan())]
logger.warning(u'Scanning entry %s failed with depth %d/%d' % (entry, depth, max_depth))
return [] # anything else
def hash_opensubtitles(path):
"""Compute a hash using OpenSubtitles' algorithm
:param string path: path
:return: hash
:rtype: string
"""
longlongformat = 'q' # long long
bytesize = struct.calcsize(longlongformat)
with open(path, 'rb') as f:
filesize = os.path.getsize(path)
filehash = filesize
if filesize < 65536 * 2:
return None
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(longlongformat, filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF # to remain as 64bit number
f.seek(max(0, filesize - 65536), 0)
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(longlongformat, filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF
returnedhash = '%016x' % filehash
logger.debug(u'Computed OpenSubtitle hash %s for %s' % (returnedhash, path))
return returnedhash
def hash_thesubdb(path):
"""Compute a hash using TheSubDB's algorithm
:param string path: path
:return: hash
:rtype: string
"""
readsize = 64 * 1024
with open(path, 'rb') as f:
data = f.read(readsize)
f.seek(-readsize, os.SEEK_END)
data += f.read(readsize)
returnedhash = hashlib.md5(data).hexdigest()
logger.debug(u'Computed TheSubDB hash %s for %s' % (returnedhash, path))
return returnedhash
+26
View File
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import test_language, test_services, test_subliminal
import unittest
suite = unittest.TestSuite([test_language.suite(), test_services.suite(), test_subliminal.suite()])
if __name__ == '__main__':
unittest.TextTestRunner().run(suite)
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from subliminal.language import Language, Country, language_set, language_list
import unittest
class LanguageListTestCase(unittest.TestCase):
def test_list_contains(self):
languages = list([Language('fr'), Language('en-US'), Language('en-GB')])
self.assertTrue(Language('fr') in languages)
self.assertTrue(Language('en-US') in languages)
self.assertTrue(Language('en') not in languages)
self.assertTrue(Language('fr-BE') not in languages)
def test_language_list_contains(self):
languages = language_list(['fr', 'en-US', 'en-GB'])
self.assertTrue(Language('fr') in languages)
self.assertTrue(Language('en-US') in languages)
self.assertTrue(Language('en') not in languages)
self.assertTrue(Language('fr-BE') in languages)
def test_list_index(self):
languages = [Language('fr'), Language('en-US'), Language('en-GB')]
self.assertTrue(languages.index(Language('fr')) == 0)
self.assertTrue(languages.index(Language('en-US')) == 1)
self.assertTrue(languages.index(Language('en-GB')) == 2)
with self.assertRaises(ValueError):
languages.index(Language('fr-BE'))
def test_language_list_index(self):
languages = language_list(['fr', 'en-US', 'en-GB'])
self.assertTrue(languages.index(Language('fr')) == 0)
self.assertTrue(languages.index(Language('en-US')) == 1)
self.assertTrue(languages.index(Language('en-GB')) == 2)
self.assertTrue(languages.index(Language('fr-BE')) == 0)
class LanguageSetTestCase(unittest.TestCase):
def test_set_contains(self):
languages = set([Language('fr'), Language('en-US'), Language('en-GB')])
self.assertTrue(Language('fr') in languages)
self.assertTrue(Language('en-US') in languages)
self.assertTrue(Language('en') not in languages)
self.assertTrue(Language('fr-BE') not in languages)
def test_language_set_contains(self):
languages = language_set(['fr', 'en-US', 'en-GB'])
self.assertTrue(Language('fr') in languages)
self.assertTrue(Language('en-US') in languages)
self.assertTrue(Language('en') not in languages)
self.assertTrue(Language('fr-BE') in languages)
def test_language_set_intersect(self):
languages = language_set(['fr', 'en-US', 'en-GB'])
self.assertTrue(len(languages & language_set([Language('en')])) == 2)
self.assertTrue(len(language_set([Language('en')]) & languages) == 2)
self.assertTrue(len(languages & language_set([Language('fr')])) == 1)
def test_language_set_substract(self):
languages = language_set(['fr', 'en-US', 'en-GB'])
self.assertTrue(len(languages - language_set(['en'])) == 1)
self.assertTrue(len(languages - language_set(['en-US'])) == 2)
self.assertTrue(len(languages - language_set(['en-US', 'fr'])) == 1)
class LanguageTestCase(unittest.TestCase):
def test_attrs(self):
language = Language('French')
self.assertTrue(language.alpha2 == 'fr')
self.assertTrue(language.alpha3 == 'fre')
self.assertTrue(language.terminologic == 'fra')
self.assertTrue(language.name == 'French')
self.assertTrue(language.french_name == u'français')
def test_eq(self):
language = Language('French')
self.assertTrue(language == Language('fr'))
self.assertTrue(language == Language('fre'))
self.assertTrue(language == Language('fra'))
self.assertTrue(language == Language('Français'))
def test_ne(self):
self.assertTrue(Language('French') != Language('en'))
def test_in(self):
self.assertTrue(Language('Portuguese (BR)') in Language('Portuguese - Brazil'))
self.assertTrue(Language('Portuguese (BR)') in Language('Portuguese'))
self.assertTrue(Language('Portuguese') not in Language('Portuguese (BR)'))
def test_with_country(self):
self.assertTrue(Language('Portuguese (BR)').country == Country('Brazil'))
self.assertTrue(Language('pt_BR').country == Country('Brazil'))
self.assertTrue(Language('fr - France').country == Country('France'))
self.assertTrue(Language('fra', country='FR').country == Country('France'))
self.assertTrue(Language('fra', country=Country('FRA')).country == Country('France'))
def test_eq_with_country(self):
self.assertTrue(Language('Portuguese (BR)') == Language('Portuguese - Brazil'))
self.assertTrue(Language('English') == Language('en'))
def test_ne_with_country(self):
self.assertTrue(Language('Portuguese') != Language('Portuguese (BR)'))
self.assertTrue(Language('English (US)') != Language('English (GB)'))
def test_hash(self):
self.assertTrue(hash(Language('French')) == hash('fre'))
def test_missing(self):
with self.assertRaises(ValueError):
Language('zzz')
class CountryTestCase(unittest.TestCase):
def test_attrs(self):
country = Country('France')
self.assertTrue(country.alpha2 == 'FR')
self.assertTrue(country.alpha3 == 'FRA')
self.assertTrue(country.name == 'France')
def test_eq(self):
country = Country('France')
self.assertTrue(country == Country('FR'))
self.assertTrue(country == Country('FRA'))
self.assertTrue(country == Country('250'))
def test_ne(self):
self.assertTrue(Country('France') != Country('GB'))
def test_hash(self):
self.assertTrue(hash(Country('France')) == hash('FRA'))
def test_missing(self):
with self.assertRaises(ValueError):
Country('ZZ')
def suite():
suite = unittest.TestSuite()
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(CountryTestCase))
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(LanguageTestCase))
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(LanguageSetTestCase))
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(LanguageListTestCase))
return suite
if __name__ == '__main__':
unittest.TextTestRunner().run(suite())
+503
View File
@@ -0,0 +1,503 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from subliminal import videos
from subliminal.exceptions import ServiceError
from subliminal.language import language_set, LANGUAGES
from subliminal.services import ServiceConfig
from subliminal.services.addic7ed import Addic7ed
from subliminal.services.bierdopje import BierDopje
from subliminal.services.opensubtitles import OpenSubtitles
from subliminal.services.podnapisi import Podnapisi
from subliminal.services.subswiki import SubsWiki
from subliminal.services.subtitulos import Subtitulos
from subliminal.services.thesubdb import TheSubDB
from subliminal.services.tvsubtitles import TvSubtitles
import os
import sys
import unittest
try:
import cPickle as pickle
except ImportError:
import pickle
cache_dir = u'/tmp/sublicache'
if not os.path.exists(cache_dir):
os.mkdir(cache_dir)
existing_video = u'/something/The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4'
class ServiceTestCase(unittest.TestCase):
def setUp(self):
self.wrong_languages = language_set(list(language_set(LANGUAGES) - self.service.languages)[:2])
def tearDown(self):
# Setting config to None allows to delete the object, which will in turn save the cache
self.config = None
@unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(service, self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(service, self.fake_file, self.languages, self.episode_keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(service, self.fake_file, self.wrong_languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_list_episode(self):
video = videos.Video.from_path(self.episode_path)
with self.service(self.config) as service:
results = service.list(video, self.languages)
self.assertTrue(len(results) > 0)
def test_list_movie(self):
video = videos.Video.from_path(self.movie_path)
with self.service(self.config) as service:
results = service.list(video, self.languages)
self.assertTrue(len(results) > 0)
def test_list_wrong_languages(self):
video = videos.Video.from_path(self.episode_path)
with self.service(self.config) as service:
results = service.list(video, self.wrong_languages)
self.assertTrue(len(results) == 0)
def test_download_episode(self):
video = videos.Video.from_path(self.episode_path)
with self.service(self.config) as service:
subtitle = service.list(video, language_set([self.episode_sublanguage]))[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
service.download(subtitle)
self.assertTrue(os.path.exists(subtitle.path))
self.assertTrue(os.path.getsize(subtitle.path) in self.episode_subfilesizes, msg='Size %d not in %r' % (os.path.getsize(subtitle.path), self.episode_subfilesizes))
os.remove(subtitle.path)
def test_download_movie(self):
video = videos.Video.from_path(self.movie_path)
with self.service(self.config) as service:
subtitle = service.list(video, language_set([self.movie_sublanguage]))[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
service.download(subtitle)
self.assertTrue(os.path.exists(subtitle.path))
self.assertTrue(os.path.getsize(subtitle.path) in self.movie_subfilesizes, msg='Size %d not in %r' % (os.path.getsize(subtitle.path), self.movie_subfilesizes))
os.remove(subtitle.path)
def test_cached_series(self):
with self.service(self.config) as service:
service.clear_cache()
service.query(self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
service.save_cache()
c = pickle.load(open(os.path.join(cache_dir, 'subliminal_%s.cache' % self.service.__name__)))
found = False
for _, cached_values in c.items():
for args, __ in cached_values.items():
if args == (self.series.lower(),):
found = True
self.assertTrue(found)
class Addic7edTestCase(ServiceTestCase):
query_tests = ['test_query_series', 'test_query_wrong_series', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = ['test_cached_series']
service = Addic7ed
def setUp(self):
super(Addic7edTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.fake_file = u'/tmp/fake_file'
self.languages = language_set(['en', 'fr'])
self.episode_path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.episode_sublanguage = 'en'
# FIXME: this is the size of the first subtitle that appears on the page
# which is the original one, not the most updated one. We should make
# sure the Addic7ed service picks up the most recent one instead
self.episode_subfilesizes = [33538, 33643]
self.episode_keywords = set(['asap', 'hdtv'])
self.series = 'The Big Bang Theory'
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
class BierDopjeTestCase(ServiceTestCase):
query_tests = ['test_query_series', 'test_query_wrong_series', 'test_query_wrong_languages',
'test_query_tvdbid', 'test_query_wrong_tvdbid', 'test_query_series_and_tvdbid']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = ['test_cached_series']
service = BierDopje
def setUp(self):
super(BierDopjeTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.episode_path = u'The Big Bang Theory/Season 05/S05E06 - The Rhinitis Revelation - HD TV.mkv'
self.episode_sublanguage = 'en'
self.episode_subfilesizes = [33469]
self.movie_path = u'Inception (2010)/Inception - 1080p.mkv'
self.movie_sublanguage = 'en'
self.movie_subfilesizes = []
self.languages = language_set(['en', 'nl'])
self.fake_file = u'/tmp/fake_file'
self.series = 'The Big Bang Theory'
self.episode_keywords = set()
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
self.tvdbid = 80379
self.wrong_tvdbid = 9999999999
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.season, self.episode, self.languages, series=self.series)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.season, self.episode, self.languages, series=self.wrong_series)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.season, self.episode, self.wrong_languages, series=self.series)
self.assertTrue(len(results) == 0)
def test_query_tvdbid(self):
with BierDopje(self.config) as service:
results = service.query(self.fake_file, self.season, self.episode, self.languages, tvdbid=self.tvdbid)
self.assertTrue(len(results) > 0)
def test_query_series_and_tvdbid(self):
with BierDopje(self.config) as service:
results = service.query(self.fake_file, self.season, self.episode, self.languages, series=self.series, tvdbid=self.tvdbid)
self.assertTrue(len(results) > 0)
def test_query_wrong_tvdbid(self):
with BierDopje(self.config) as service:
results = service.query(self.fake_file, self.season, self.episode, self.languages, tvdbid=self.wrong_tvdbid)
self.assertTrue(len(results) == 0)
def test_cached_series(self):
with self.service(self.config) as service:
service.clear_cache()
service.query(self.fake_file, self.season, self.episode, self.languages, series=self.series)
service.save_cache()
c = pickle.load(open(os.path.join(cache_dir, 'subliminal_%s.cache' % self.service.__name__)))
found = False
for _, cached_values in c.items():
for args, __ in cached_values.items():
if args == (self.series.lower(),):
found = True
self.assertTrue(found)
class OpenSubtitlesTestCase(ServiceTestCase):
query_tests = ['test_query_query', 'test_query_imdbid', 'test_query_hash', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = []
service = OpenSubtitles
def setUp(self):
super(OpenSubtitlesTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.languages = language_set(['en', 'fr'])
self.fake_file = u'/tmp/fake_file'
self.episode_path = existing_video
self.episode_sublanguage = 'en'
self.episode_subfilesizes = [33585, 33547, 33563, 33601]
self.movie = 'Inception'
self.imdbid = '1375666'
self.wrong_imdbid = '9999999'
self.hash = '51e57c4e8fd77990'
self.size = 882571264L
def test_query_query(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, query=self.movie)
self.assertTrue(len(results) > 0)
def test_query_imdbid(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, imdbid=self.imdbid)
self.assertTrue(len(results) > 0)
def test_query_hash(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, moviehash=self.hash, size=self.size)
self.assertTrue(len(results) > 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, moviehash=self.hash, size=self.size)
self.assertTrue(len(results) == 0)
class PodnapisiTestCase(ServiceTestCase):
query_tests = ['test_query', 'test_query_wrong_languages']
list_tests = [] #'test_list', 'test_list_wrong_languages'
download_tests = [] #'test_download'
cache_tests = []
service = Podnapisi
def setUp(self):
super(PodnapisiTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.languages = language_set(['en', 'fr'])
self.fake_file = u'/tmp/fake_file'
self.path = existing_video
self.hash = 'e1b45885346cfa0b'
def test_query(self):
with Podnapisi(self.config) as service:
results = service.query(self.fake_file, self.languages, moviehash=self.hash)
self.assertTrue(len(results) > 0)
def test_query_wrong_languages(self):
with Podnapisi(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, moviehash=self.hash)
self.assertTrue(len(results) == 0)
class SubsWikiTestCase(ServiceTestCase):
query_tests = ['test_query_series', 'test_query_movie', 'test_query_wrong_parameters', 'test_query_wrong_series', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_movie', 'test_list_wrong_languages']
download_tests = ['test_download_episode', 'test_download_movie']
cache_tests = []
service = SubsWiki
def setUp(self):
super(SubsWikiTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.fake_file = u'/tmp/fake_file'
self.languages = language_set(['en', 'es'])
self.movie_path = u'Soul Surfer (2011)/Soul.Surfer.(2011).DVDRip.XviD-TWiZTED.mkv'
self.movie_sublanguage = 'es'
self.movie_subfilesizes = [87528]
self.movie_keywords = set(['twizted'])
self.movie = u'Soul Surfer'
self.movie_year = 2011
self.episode_path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.episode_sublanguage = 'es'
self.episode_subfilesizes = [34040]
self.episode_keywords = set(['asap', 'hdtv'])
self.series = 'The Big Bang Theory'
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_movie(self):
with SubsWiki(self.config) as service:
results = service.query(self.fake_file, self.languages, keywords=self.movie_keywords, movie=self.movie, year=self.movie_year)
self.assertTrue(len(results) > 0)
def test_query_wrong_parameters(self):
with SubsWiki(self.config) as service:
self.assertRaises(ServiceError, service.query,
self.fake_file, self.languages, keywords=self.movie_keywords, movie=self.movie, series=self.series)
class SubtitulosTestCase(ServiceTestCase):
query_tests = ['test_query_series', 'test_query_wrong_series', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = []
service = Subtitulos
def setUp(self):
super(SubtitulosTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.fake_file = u'/tmp/fake_file'
self.languages = language_set(['en', 'es'])
self.episode_path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.episode_sublanguage = 'en'
self.episode_subfilesizes = [32986]
self.episode_keywords = set(['asap', 'hdtv'])
self.series = 'The Big Bang Theory'
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
class TheSubDBTestCase(ServiceTestCase):
query_tests = ['test_query', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = []
service = TheSubDB
def setUp(self):
super(TheSubDBTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.episode_path = existing_video
self.episode_sublanguage = 'en'
self.episode_subfilesizes = [33536]
self.hash = u'edc1981d6459c6111fe36205b4aff6c2'
self.languages = language_set(['en', 'nl'])
self.fake_file = u'/tmp/fake_file'
def test_query(self):
with TheSubDB(self.config) as service:
results = service.query(self.fake_file, self.hash, self.languages)
self.assertTrue(len(results) > 0)
def test_query_wrong_languages(self):
with TheSubDB(self.config) as service:
results = service.query(self.fake_file, self.hash, self.wrong_languages)
self.assertTrue(len(results) == 0)
@unittest.skipUnless(os.path.exists(existing_video), 'No existing video')
def test_list_episode(self):
super(TheSubDBTestCase, self).test_list_episode()
@unittest.skipUnless(os.path.exists(existing_video), 'No existing video')
def test_download_episode(self):
super(TheSubDBTestCase, self).test_download_episode()
class TvSubtitlesTestCase(ServiceTestCase):
query_tests = ['test_query_series', 'test_query_wrong_series', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = ['test_cached_series']
service = TvSubtitles
def setUp(self):
super(TvSubtitlesTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.fake_file = u'/tmp/fake_file'
self.languages = language_set(['en', 'es'])
self.episode_path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.episode_sublanguage = 'en'
self.episode_subfilesizes = [33078]
self.episode_keywords = set(['asap', 'hdtv'])
self.series = 'The Big Bang Theory'
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
TESTCASES = [Addic7edTestCase, BierDopjeTestCase, OpenSubtitlesTestCase, PodnapisiTestCase, SubsWikiTestCase,
SubtitulosTestCase, TheSubDBTestCase, TvSubtitlesTestCase]
def query_suite():
suite = unittest.TestSuite()
for testcase in TESTCASES:
suite.addTests(map(testcase, testcase.query_tests))
return suite
def list_suite():
suite = unittest.TestSuite()
for testcase in TESTCASES:
suite.addTests(map(testcase, testcase.list_tests))
return suite
def download_suite():
suite = unittest.TestSuite()
for testcase in TESTCASES:
suite.addTests(map(testcase, testcase.download_tests))
return suite
def cache_suite():
suite = unittest.TestSuite()
for testcase in TESTCASES:
suite.addTests(map(testcase, testcase.cache_tests))
return suite
def suite():
return unittest.TestSuite([query_suite(), list_suite(), download_suite(), cache_suite()])
if __name__ == '__main__':
unittest.TextTestRunner(verbosity=2).run(suite())
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from subliminal import Pool, list_subtitles, download_subtitles
import os
import time
import unittest
cache_dir = u'/tmp/sublicache'
if not os.path.exists(cache_dir):
os.mkdir(cache_dir)
existing_video = u'/something/The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4'
class ApiTestCase(unittest.TestCase):
def test_list_subtitles(self):
results = list_subtitles(existing_video, languages=['en', 'fr'], cache_dir=cache_dir, max_depth=3)
self.assertTrue(len(results) > 0)
def test_download_subtitles(self):
results = download_subtitles(existing_video, languages=['en', 'fr'], cache_dir=cache_dir, max_depth=3)
self.assertTrue(len(results) == 1)
for video, subtitles in results.iteritems():
self.assertTrue(video.release == existing_video)
self.assertTrue(len(subtitles) == 1)
for subtitle in subtitles:
self.assertTrue(os.path.exists(subtitle.path))
os.remove(subtitle.path)
def test_download_multi_subtitles(self):
results = download_subtitles(existing_video, languages=['en', 'fr'], cache_dir=cache_dir, max_depth=3, multi=True)
self.assertTrue(len(results) == 1)
for video, subtitles in results.iteritems():
self.assertTrue(video.release == existing_video)
self.assertTrue(len(subtitles) == 2)
for subtitle in subtitles:
self.assertTrue(os.path.exists(subtitle.path))
os.remove(subtitle.path)
class AsyncTestCase(unittest.TestCase):
def test_pool(self):
p = Pool(4)
self.assertTrue(len(p.workers) == 4)
for w in p.workers:
self.assertTrue(w.isAlive() == False)
p.start()
for w in p.workers:
self.assertTrue(w.isAlive() == True)
p.stop()
p.join()
time.sleep(0.2) # so terminate is finished on Worker and proper Thread methods finished
for w in p.workers:
self.assertTrue(w.isAlive() == False)
def test_list_subtitles(self):
with Pool(4) as p:
results = p.list_subtitles(existing_video, languages=['en', 'fr'], cache_dir=cache_dir, max_depth=3)
self.assertTrue(len(results) > 0)
def test_download_subtitles(self):
with Pool(4) as p:
results = p.download_subtitles(existing_video, languages=['en', 'fr'], cache_dir=cache_dir, max_depth=3)
self.assertTrue(len(results) == 1)
for video, subtitles in results.iteritems():
self.assertTrue(video.release == existing_video)
self.assertTrue(len(subtitles) == 1)
for subtitle in subtitles:
self.assertTrue(os.path.exists(subtitle.path))
os.remove(subtitle.path)
def suite():
suite = unittest.TestSuite()
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(ApiTestCase))
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AsyncTestCase))
return suite
if __name__ == '__main__':
unittest.TextTestRunner().run(suite())