Compare commits

...

410 Commits

Author SHA1 Message Date
Antoine Bertin 12443b5d78 Release 2.0.3 2016-06-10 20:38:23 +02:00
Antoine Bertin 1f246d8c03 Fix clearing cache in CLI 2016-06-10 20:37:52 +02:00
Antoine Bertin e2bda1cfce Release 2.0.2 2016-06-06 22:36:59 +02:00
Antoine Bertin 5d28f14978 Fix for dogpile.cache>=0.6.0 2016-06-06 22:35:58 +02:00
Antoine Bertin 25213e3d0d Fix missing sphinx_rtd_theme dependency 2016-06-06 22:30:13 +02:00
Antoine Bertin 0322e920ed Release 2.0.1 2016-06-06 19:41:49 +02:00
Antoine Bertin fd02bbe4d6 Use beautifulsoup>=4.4.0 2016-06-06 19:39:28 +02:00
Antoine Bertin 218318286b Release 2.0.0 2016-06-04 20:39:35 +02:00
Antoine Bertin 9620b71d08 Merge pull request #665 from h3llrais3r/score-calculation
New score equations
2016-06-04 20:24:38 +02:00
h3llrais3r c5a911de4c Fix usage.rst 2016-05-27 21:45:30 +02:00
h3llrais3r a7b9ec0837 New score equations
With the new score calculation, a score increasing match (like
hearing_impaired) should not influence the score matching of the video
properties. Score increasing matches should only count as 1 and the
lowest video property match should be at least 1 more than the sum of
all score increasing matches (currently only hearing_impaired).
This fixes https://github.com/Diaoul/subliminal/issues/664
2016-05-27 21:09:34 +02:00
Antoine Bertin e2331bef2c Fix for titles without year in legendastv 2016-05-21 08:37:51 +02:00
Antoine Bertin 14707b68ca Ensure scan_videos continues if unrar is not installed 2016-05-01 18:44:54 +02:00
Antoine Bertin 37b05f710f Update omdb cassettes 2016-04-30 12:20:56 +02:00
Antoine Bertin e842729f5e Use 3.5 for integration tests in travis 2016-04-30 12:14:55 +02:00
Antoine Bertin e233448ab5 Add TooManyRequests safeguard on 304 in addic7ed 2016-04-30 12:01:43 +02:00
Antoine Bertin 5bf82fa7ac Fix failing tests 2016-04-29 16:21:10 +02:00
Antoine Bertin 431f71fb64 Fix for wrong alpha2 subtitle languages in subtitle names 2016-04-25 00:16:25 +02:00
Antoine Bertin b3a8081df7 Add original_series detection in tvdb refiner 2016-04-25 00:12:40 +02:00
Antoine Bertin 67a48e07a2 Use original series name for tvdb refiner 2016-04-14 01:13:59 +02:00
Antoine Bertin b48c0c946b Merge branch 'develop' of github.com:Diaoul/subliminal into develop 2016-04-11 23:22:08 +02:00
Antoine Bertin d5b4a4f226 Improve tvdb refiner 2016-04-11 23:21:51 +02:00
Antoine Bertin a71cefb583 Merge pull request #632 from ratoaq2/patch-1
Missing shooter provider in the docs
2016-04-11 15:49:09 +02:00
Rato a413c77b32 Missing shooter provider in the docs 2016-04-11 15:43:57 +02:00
Antoine Bertin 5d6c19002d Merge pull request #610 from ratoaq2/legendastv_season_detection
Improving season detection and handling missing season info
2016-04-10 13:06:39 +02:00
Antoine Bertin a6b8068b7c Merge branch 'develop' of github.com:Diaoul/subliminal into develop 2016-04-10 13:04:53 +02:00
Antoine Bertin 70a647813b Refactor subscenter and fix when no title is found 2016-04-10 13:03:34 +02:00
Antoine Bertin ec5aaffe26 Typo in shooter provider 2016-04-10 13:01:21 +02:00
Rato 2e07fc17f2 Improving LegendasTv season detection and handling missing season information 2016-04-09 21:11:29 +02:00
Antoine Bertin 66fd2d10ac Merge pull request #626 from ratoaq2/shooter_fixes
Adding missing hash and id property in shooter provider
2016-04-09 20:56:15 +02:00
Rato 5218c9ff45 Adding missing hash and id property in shooter provider. Credits to @medariox 2016-04-09 19:26:46 +02:00
Antoine Bertin f53e6488db Fix wrong expiration time in legendastv provider 2016-04-09 17:53:29 +02:00
Antoine Bertin d33bd854e0 Fix timestamp regex in legendastv provider 2016-04-09 17:52:58 +02:00
Antoine Bertin abccb4387e Add pytz dependency 2016-04-09 17:52:31 +02:00
Antoine Bertin b9c4c86e1f Improve caching for legendastv provider
- Archive pages are cached for 15 minutes only
- Releases expiration time is based on archive timestamp
2016-04-09 12:32:09 +02:00
Antoine Bertin 25ece03102 Use raw strings for regular expressions 2016-04-08 23:53:18 +02:00
Antoine Bertin 08fbe4b43b Code style in legendastv provider 2016-04-08 23:52:44 +02:00
Antoine Bertin 104004535f Merge pull request #615 from ratoaq2/legendastv_improvements
Avoiding exceptions when legendastv provider returns invalid year/season
2016-04-08 23:30:02 +02:00
Antoine Bertin 8727137847 Merge pull request #611 from ratoaq2/legendastv_titles_with_dots
Handling titles with dots (legendastv provider)
2016-04-08 23:26:07 +02:00
Antoine Bertin 7c2023b4b3 Improve tvdb refiner
Make sure we have an exact match on the series
2016-04-08 21:09:06 +02:00
Antoine Bertin 1d085475d9 Use dots for version 2016-04-08 21:05:04 +02:00
Antoine Bertin 3ebb63bc80 Update opensubtitles cassettes 2016-04-08 21:03:35 +02:00
Rato c0ce9cf187 Avoiding exceptions when legendastv provider returns year or invalid season 2016-04-02 21:36:12 +02:00
Rato d7649eac1f Fixing searching titles with dots for LegendasTv provider 2016-04-02 19:28:44 +02:00
Antoine Bertin f80eb29a5a Update HISTORY 2016-03-31 23:30:51 +02:00
Antoine Bertin ecd7a6679b Release 2.0-rc1 2016-03-31 23:28:12 +02:00
Antoine Bertin b6ed77a56c Be more permissive for release group matching 2016-03-31 23:15:40 +02:00
Antoine Bertin 213ffcb41a Fix typo 2016-03-31 23:06:32 +02:00
Antoine Bertin 79de8e7f93 Fix tvdb refiner when search returned no results 2016-03-31 23:04:46 +02:00
Antoine Bertin a732b0c75b Default season to 1 for miniseries 2016-03-31 22:44:39 +02:00
Antoine Bertin a38527b722 Update documentation 2016-03-31 22:33:54 +02:00
Antoine Bertin 1d9ad95aaf Remove duplicate check on encoding in Subtitle class 2016-03-31 21:07:17 +02:00
Antoine Bertin 9785bc9746 Add legendastv provider 2016-03-31 20:18:24 +02:00
Antoine Bertin 328e342b9c Fix extensions unittests 2016-03-24 07:52:35 +01:00
Antoine Bertin 7ed39c15dd Fix core unittest missing shooter 2016-03-23 10:40:01 +01:00
Antoine Bertin 7cca57332b Add shooter to extensions 2016-03-23 10:39:43 +01:00
Antoine Bertin cef6cb8826 Remove unused imports in shooter 2016-03-23 10:39:35 +01:00
Antoine Bertin 1faf26547b Ensure cassette paths are resolved at import time 2016-03-23 10:39:13 +01:00
Antoine Bertin 54555b2b16 Move cassettes from api to core 2016-03-23 10:38:00 +01:00
Antoine Bertin 152a5f12c3 Update docs usage cassette 2016-03-23 10:37:18 +01:00
Antoine Bertin b530c0e875 Rename server to server_url in subscenter provider 2016-03-23 09:51:45 +01:00
Antoine Bertin c03be0e2f0 Merge branch 'develop' of https://github.com/bcse/subliminal into bcse-develop 2016-03-23 09:49:59 +01:00
Antoine Bertin ceb641daa5 Update omdb unittests and cassettes 2016-03-21 22:36:52 +01:00
Antoine Bertin 10c9ab7a8f Do not sort refiners in config 2016-03-20 23:33:55 +01:00
Antoine Bertin 63dd6a1f3c Mention minimum python version in the docs 2016-03-20 23:21:13 +01:00
Antoine Bertin 460528bf87 Add configuration for refiners 2016-03-20 23:11:36 +01:00
Antoine Bertin ccb90734bd Fix call to scan_videos in cli 2016-03-20 21:17:23 +01:00
Antoine Bertin 2ed731ce15 Remove unused kwargs in scan_videos 2016-03-20 21:14:01 +01:00
Antoine Bertin c0547a63ca Update refiners docs 2016-03-20 19:29:29 +01:00
Antoine Bertin 54b9439e54 Update kwargs docs 2016-03-20 19:29:10 +01:00
Antoine Bertin 91267191f1 Add a refiner option in CLI 2016-03-20 12:10:35 +01:00
Antoine Bertin f8854fe89a Add a warning when some providers are discarded in CLI 2016-03-20 12:09:40 +01:00
Antoine Bertin 2ee9e6333b Default refine arguments inside the function 2016-03-20 12:09:16 +01:00
Antoine Bertin f6395e4022 Sort results from tvdb 2016-03-20 11:21:40 +01:00
Antoine Bertin 160b5f8416 Add user agent to requests made to tvdb in unittests 2016-03-20 11:17:04 +01:00
Antoine Bertin 34ce399d7c Remove unused tvdb cassette 2016-03-20 11:15:17 +01:00
Antoine Bertin 68b7b57bbd Add more time to refresh token in tvdb unittests 2016-03-20 11:14:39 +01:00
Antoine Bertin f40e79a78b Fix invalid extension tests 2016-03-07 23:40:16 +01:00
Antoine Bertin 2039cf78d6 Avoid encoding issues in logging 2016-03-07 23:22:07 +01:00
Antoine Bertin 5926fbe2bd Reorder refiners 2016-03-07 23:21:27 +01:00
Antoine Bertin 5f4d83ddc2 Fix doctests 2016-03-07 23:10:19 +01:00
Antoine Bertin 25644ede67 Remove some comments in search_external_subtitles 2016-03-07 23:10:11 +01:00
Antoine Bertin 2a68d04d60 Merge branch 'develop' of https://github.com/brp-david/subliminal into brp-david-develop 2016-03-07 23:07:50 +01:00
Antoine Bertin 4486c44020 Fix addic7ed unittests 2016-02-29 23:02:18 +01:00
Antoine Bertin 8f39a2037f Remove duplicate logging 2016-02-29 23:02:09 +01:00
Antoine Bertin e9e0c60bb1 Move refine to its own function 2016-02-29 23:01:55 +01:00
Antoine Bertin 768f4a43bc Add verbosity to refiners 2016-02-29 23:00:41 +01:00
Antoine Bertin c3c0c4584f Replace dot by space instead of removing it in sanitize 2016-02-25 00:40:26 +01:00
Antoine Bertin b5b6dc827a Add SubFileName information in opensubtitles 2016-02-25 00:39:52 +01:00
Antoine Bertin 6a4124c8b1 Improve logging of found subtitles in opensubtitles 2016-02-25 00:37:08 +01:00
Antoine Bertin b709799039 Merge branch 'develop' of github.com:Diaoul/subliminal into develop 2016-02-24 23:37:37 +01:00
Antoine Bertin c3d85a0b6d Refactoring 2016-02-24 23:37:24 +01:00
Antoine Bertin a8033f386c Fix extension check in metadata refiner 2016-02-24 23:36:25 +01:00
Antoine Bertin 15993ce647 Use logging string formatting 2016-02-24 23:35:29 +01:00
Antoine Bertin 484788757b Add a constructor docstring to RegistrableExtensionManager 2016-02-24 23:32:38 +01:00
Antoine Bertin b36971ed49 Add kwargs to RegistrableExtensionManager 2016-02-24 23:32:17 +01:00
Antoine Bertin b6ed4103ef Fix extensions registering
Not making a copy of the list resulted in updating
the ENTRY_POINT_CACHE which is not what we want.
2016-02-24 23:31:36 +01:00
Antoine Bertin 58fb9728ad Typo in usage docs 2016-02-24 23:29:53 +01:00
Antoine Bertin f7526d338c pep8 in setup.py 2016-02-24 23:24:15 +01:00
Antoine Bertin aa72a205f2 Refactor encoding validation in Subtitle 2016-02-21 23:41:55 +01:00
Antoine Bertin 2d97781fe5 Documentation fixes 2016-02-21 23:41:43 +01:00
Antoine Bertin d5d191847f Require appdirs 1.3 due to issues with appauthor 2016-02-21 23:34:11 +01:00
Antoine Bertin 8cd6a05301 Add nitpicky flag to sphinx 2016-02-21 23:33:39 +01:00
Antoine Bertin f5435b1c81 Use python 3.5 as documentation reference 2016-02-21 23:33:29 +01:00
Antoine Bertin 0a1a164cb1 Use relative imports in thesubdb 2016-02-21 22:51:45 +01:00
Antoine Bertin 200ff42385 Update refiners documentation 2016-02-21 22:51:32 +01:00
Antoine Bertin ca6a041ac8 Add metadata refiner 2016-02-21 22:51:09 +01:00
Antoine Bertin bf74bc0a0b Move extension managers to an extensions module 2016-02-21 22:50:35 +01:00
Antoine Bertin b58f072348 Update docs 2016-02-21 22:49:31 +01:00
Grey Lee a449ce3bb8 Merge branch 'develop' of https://github.com/Diaoul/subliminal into develop 2016-02-15 19:06:12 +08:00
Antoine Bertin d7d7a2c49d Typo 2016-02-12 17:19:49 +01:00
Grey Lee 051545444b Update cassettes for shooter.cn 2016-02-11 14:00:29 +08:00
Grey Lee dfb3943643 Add Shooter.cn provider 2016-02-11 13:49:27 +08:00
David Lindahl a00af07e58 Merged develop 2016-02-10 12:19:39 +01:00
David Lindahl c79908b320 Merge branch 'upstream-develop' into develop
Conflicts:
	subliminal/video.py
2016-02-10 10:25:03 +01:00
Antoine Bertin 864ecf4c7c Reverse __short_version__ logic 2016-02-09 19:16:57 +01:00
Antoine Bertin e0a996c090 Improve setup.py 2016-02-09 19:16:36 +01:00
Antoine Bertin 8dd1f2344c Rename api to core 2016-02-09 19:15:44 +01:00
Antoine Bertin 4e326452d5 Update scores 2016-02-09 18:50:21 +01:00
Antoine Bertin b0bd236b40 Add refiners 2016-02-09 17:25:18 +01:00
Antoine Bertin 202eb610af Update api cassettes 2016-02-09 17:24:46 +01:00
Antoine Bertin 6dfb479da5 Update tvsubtitles cassettes 2016-02-09 17:20:41 +01:00
Antoine Bertin b3f8a3ca4b Update subscenter cassettes 2016-02-09 17:19:52 +01:00
Antoine Bertin daacc015b8 Update thesubdb cassettes 2016-02-09 17:19:15 +01:00
Antoine Bertin 62b5ab6b5a Update podnapisi cassettes 2016-02-09 17:17:08 +01:00
Antoine Bertin 2984bedcd0 Fix addic7ed test_query_parsing_dash unittest 2016-02-09 17:16:19 +01:00
Antoine Bertin e08a0fa847 Update opensubtitles cassettes 2016-02-09 17:15:58 +01:00
Antoine Bertin 3705d5510d Update addic7ed cassettes 2016-02-09 17:14:38 +01:00
Antoine Bertin 727772fe31 Do not override default headers 2016-02-09 17:05:50 +01:00
Antoine Bertin a7b13a15e5 Use str for imdb_id, add tvdb_id and imdb_id for series in Episode 2016-02-09 16:42:56 +01:00
Antoine Bertin 9d7ed43add Major version bump 2016-02-09 16:39:19 +01:00
Antoine Bertin 24f18379e4 Update license to 2016 2016-02-09 16:16:21 +01:00
David f339c90fc3 Added short name of flag 2016-02-09 12:31:31 +01:00
David Lindahl 4c32df1f22 Simplified rar scanning 2016-02-08 21:27:32 +01:00
David Lindahl 47a18336af Support for scanning archives for video files 2016-02-06 23:35:02 +01:00
Antoine Bertin 5c23a0b375 Add original_series to Episode 2016-02-05 20:29:22 +01:00
Antoine Bertin 5e8cbad452 Refactor title sanitization 2016-02-05 17:59:35 +01:00
Antoine Bertin 2cd305ab3e Remove unused monkeypatch fixture in unittest 2016-02-05 16:31:04 +01:00
Antoine Bertin e038418bd3 Use kwargs for video subclasses 2016-02-05 16:29:04 +01:00
Antoine Bertin 6a1323b218 Use kwargs in scan_videos 2016-02-05 16:25:30 +01:00
Antoine Bertin ae1aaaddb8 Remove unused CACHE_VERSION 2016-02-05 10:06:17 +01:00
Antoine Bertin e97ad5cd06 Update README with new scan_videos age parameter 2016-02-04 15:02:20 +01:00
Antoine Bertin c0fc1e9a50 Use __short_version__ instead of get_version 2016-02-04 10:59:21 +01:00
Antoine Bertin f245383c28 Add year match in podnapisi for episodes
Although it shouldn't change anything because the subtitle always
contains the year information so no match on None can be made.
2016-02-04 00:56:55 +01:00
Antoine Bertin 89a2f7aaef Add year match in opensubtitles for episodes
Although it was already matched by guess_matches, this will add
the match even if guess_matches fails.
2016-02-04 00:45:29 +01:00
Antoine Bertin 78a881fe56 Use appdirs and correct cache dir for cli 2016-02-03 19:39:21 +01:00
Antoine Bertin 680bc12303 Refer to nautilus-subliminal in usage documentation 2016-02-03 19:38:55 +01:00
Antoine Bertin 05efb826b8 Lower requirement of futures 2016-02-03 19:30:25 +01:00
Antoine Bertin fdd0da3c43 Use sphinxcontrib-programoutput to document CLI 2016-02-02 22:32:37 +01:00
Antoine Bertin c9344822a3 Merge branch 'score' into develop 2016-02-02 22:32:15 +01:00
Antoine Bertin 0d15c2db01 Merge branch 'async' into develop 2016-02-02 19:13:31 +01:00
Antoine Bertin 272c36ac65 Refactor scoring 2016-02-02 19:10:48 +01:00
Antoine Bertin 8c1ab6cce7 Typo in documentation 2016-01-31 18:35:17 +01:00
Antoine Bertin b5c1b2e912 Fix documentation warning about with not being a keyword 2016-01-31 18:34:59 +01:00
Antoine Bertin 43ae916e96 Add AsyncProviderPool 2016-01-31 18:24:31 +01:00
Antoine Bertin c431c85abe Fix age in scan_videos 2016-01-30 18:02:58 +01:00
Antoine Bertin 5a3971a304 Remove transifex-client dev dependency 2016-01-30 17:28:49 +01:00
Antoine Bertin 5331c8d44e Add installation instructions 2016-01-29 01:05:31 +01:00
Antoine Bertin d125ffaae5 Fix for dashes in series name in addic7ed 2016-01-29 00:30:11 +01:00
Antoine Bertin 29b5e782de Merge branch 'develop' of github.com:Diaoul/subliminal into develop 2016-01-28 23:59:24 +01:00
Antoine Bertin 163115da88 Add age parameter to scan_videos to improve performance 2016-01-28 23:59:04 +01:00
Antoine Bertin 25a1ce1a5f Merge pull request #564 from Toilal/guessit2
Update guessit to 2.0
2016-01-28 20:50:47 +01:00
Toilal a5a7eb6092 Update guessit to 2.0 2016-01-28 01:47:02 +01:00
Antoine Bertin 17cce96721 Merge branch 'master' into develop 2016-01-04 12:01:07 +01:00
Antoine Bertin 615185e373 Use develop branch for badges 2016-01-04 11:59:15 +01:00
Antoine Bertin ab29e34f5a Improve query and add tag in opensubtitles 2016-01-04 10:06:00 +01:00
Antoine Bertin 101da7367e Allow subliminal to run without install 2016-01-03 21:40:37 +01:00
Antoine Bertin 75faf5c0fd Update cassettes for 1.2 2016-01-03 16:49:33 +01:00
Antoine Bertin 71f44a0d77 Remove unused import 2016-01-03 16:36:18 +01:00
Antoine Bertin 8ce2f70370 Merge branch 'hotfix' into develop 2016-01-03 11:24:09 +01:00
Antoine Bertin bd61a43b2f Release 1.1.1 2016-01-03 11:22:15 +01:00
Antoine Bertin 7ffa1e6b23 Catch all errors when parsing metadata with enzyme 2016-01-03 10:51:10 +01:00
Antoine Bertin fe29f5e2f3 Mention Nemo in README 2016-01-03 01:34:44 +01:00
Antoine Bertin cd10f3f07a Move nautilus support to its own project 2016-01-02 01:35:01 +01:00
Antoine Bertin 1d39627311 Update cli user docs 2015-12-30 20:51:15 +01:00
Antoine Bertin 006ef3e8c8 Switch to 1.2.dev0 2015-12-29 23:43:48 +01:00
Antoine Bertin f799137483 Release 1.1 2015-12-29 23:39:28 +01:00
Antoine Bertin 4dadafb87f Fix unittests 2015-12-29 21:19:20 +01:00
Antoine Bertin e85c21f40e Fix pep8 in subscenter 2015-12-29 20:01:28 +01:00
Antoine Bertin e5c50242fa Use pytest-runner 2015-12-29 20:01:06 +01:00
Antoine Bertin 2eda69d429 Keep releases sorted in subscenter 2015-12-28 21:01:40 +01:00
Antoine Bertin 6e027133cc Fix api unittests 2015-12-28 17:02:45 +01:00
Antoine Bertin eb49003bd6 Clean README 2015-12-28 16:25:42 +01:00
Antoine Bertin 707b189ab2 Clean up subscenter 2015-12-28 16:23:49 +01:00
Antoine Bertin dfb0b85877 Remove Podnapisi video_types override 2015-12-28 16:22:12 +01:00
Antoine Bertin e0b7ce46cb Use str instead of string in docstrings 2015-12-28 16:21:37 +01:00
Antoine Bertin 5912f366f8 Remove leftover print statement in unittests 2015-12-28 16:21:18 +01:00
Antoine Bertin 8b3786a875 Fix sanitized_string_equal when passing None 2015-12-28 16:20:24 +01:00
Antoine Bertin 815cee81c5 Merge remote-tracking branch 'ofir123/subscenter_support' into subscenter 2015-12-27 20:25:07 +01:00
Antoine Bertin 3b08b452a0 Disable napiproject 2015-12-27 17:02:28 +01:00
Antoine Bertin b03f97c91e Fix typo in usage documentation 2015-11-07 18:17:04 +01:00
Antoine Bertin d62a09beba Restrict to guessit<2.0 2015-11-05 23:18:10 +01:00
Antoine Bertin 90ebdebe99 Update HISTORY 2015-11-05 23:17:38 +01:00
Antoine Bertin 2e37b94bd3 Add support for searching subtitles in a separate directory 2015-11-05 23:16:46 +01:00
Antoine Bertin f10cbc04e2 Fix shows with colon in name in addic7ed provider 2015-11-05 11:06:14 +01:00
Antoine Bertin 85de66bcf2 Update translations 2015-10-30 21:13:25 +01:00
Antoine Bertin a98b5a2a04 Code style 2015-10-30 21:12:34 +01:00
Antoine Bertin 51cf652fe2 Simplify instructions to generate the po template file 2015-10-30 21:12:19 +01:00
Antoine Bertin ae117cb383 Add support for transifex client 2015-10-30 21:11:27 +01:00
Antoine Bertin 7ec08b2dcb Update translations 2015-10-30 17:51:17 +01:00
Antoine Bertin e721d6c295 Fix logging message in download_best_subtitles 2015-10-30 17:41:43 +01:00
Antoine Bertin 9d9bf92a4f Merge pull request #520 from mouchar/develop
Get enconding from OpenSubtitles provider
2015-10-30 17:30:33 +01:00
Antoine Bertin fadf3d935f Add dev-requirements.txt 2015-10-30 17:23:48 +01:00
Antoine Bertin 06aae43d7f Update unittests and cassettes 2015-10-30 10:10:39 +01:00
Antoine Bertin 9b81449dbd Fix id property in thesubdb provider 2015-10-30 10:09:59 +01:00
Antoine Bertin c369e29975 Fix log messages for downloading subtitles in podnapisi and thesubdb 2015-10-30 09:06:41 +01:00
Antoine Bertin 1a00f6fe9a Add user-agent in requests of napiprojekt provider 2015-10-30 09:05:48 +01:00
Antoine Bertin b2c38e24a2 Merge branch 'develop' of github.com:Diaoul/subliminal into develop 2015-10-30 08:57:37 +01:00
Antoine Bertin 19b3fe8495 Merge pull request #532 from bogdal/fix/napiprojekt-tests
Fix napiprojekt tests
2015-10-30 08:27:22 +01:00
Adam Bogdał 7752b92588 Restore hash of scanned movie file 2015-10-30 02:48:50 +01:00
Adam Bogdał cddeb3e552 Set appropriate subhash 2015-10-30 02:27:11 +01:00
Adam Bogdał 84eb32a4f1 Use real file hash 2015-10-30 02:22:16 +01:00
Antoine Bertin 7779fc0a21 Use new string sanitizing functions in addic7ed provider 2015-10-29 23:30:16 +01:00
Antoine Bertin 1c8f7c1954 Use real name for marvels agents of shield 2015-10-29 23:28:59 +01:00
Antoine Bertin 9fc92ab065 Fix sanitize string logic 2015-10-29 23:27:24 +01:00
Antoine Bertin ea8b646c96 Add a TooManyRequests exception in addic7ed provider 2015-10-29 23:22:59 +01:00
Antoine Bertin bc218fa138 Fix for series name with special characters in addic7ed provider 2015-10-29 22:25:28 +01:00
Antoine Bertin 2ef474960b Improve matching on titles 2015-10-29 22:22:49 +01:00
Robert Moucha fe6cfa663a Get enconding from OpenSubtitles provider
OpenSubtitles returns 'SubEncoding' so we can avoid guess_encoding()

* added condition on encoding to test_download_subtitle
* Safe handling of corrupted encoding names
2015-10-21 14:54:57 +02:00
Antoine Bertin 769b16de83 Support python 3.5 2015-10-20 20:24:31 +02:00
Antoine Bertin 4e8ea6587b Merge pull request #508 from ratoaq2/patch-1
Fix for #507
2015-10-20 20:01:52 +02:00
ofir123 4eb23d6512 Removed title guessing. 2015-10-04 12:57:54 +03:00
ofir123 b756704121 Improved title search and added cache. 2015-10-02 17:59:30 +03:00
allanjones 192b7fce1d Fix for #507: TheSubDB pt is actually pt-BR 2015-09-22 14:29:36 +02:00
ofir123 1c35aac8ce Fixed CR notes. 2015-09-22 15:22:29 +03:00
ofir123 99a071f486 Fixed tests. 2015-09-14 14:44:06 +03:00
Antoine Bertin 62e71e4c75 Created and pushed by LingoHub. Project: 'subliminal' by User: 'diaoulael@gmail.com'. 2015-09-14 07:24:40 +02:00
ofir123 e44944c538 Fixed Python 3.4 compatibility. 2015-09-13 23:34:18 +03:00
ofir123 2dd9409f9c Sorted everything alphabetically. 2015-08-30 01:23:11 +03:00
ofir123 5d2292092f Fixed tests and refreshed all subscenter cassettes. 2015-08-29 20:27:07 +03:00
ofir123 9fb3576bb2 Zipfile and entry point fix. 2015-08-29 19:56:33 +03:00
ofir123 04e3512f71 Updated global test_api file. 2015-08-29 19:11:02 +03:00
ofir123 327a40ea75 Added tests file and cassettes. 2015-08-29 19:05:55 +03:00
ofir123 dc59b44371 Added the new SubsCenter provider, and updated CLI and docs accordingly. 2015-08-29 14:27:44 +03:00
Diaoul 979bc39522 Add gitter badge 2015-08-27 22:56:19 +02:00
Diaoul db1c404a02 Code style 2015-08-27 18:49:58 +02:00
Diaoul e05297b09f Update opensubtitles cassettes 2015-08-27 18:49:54 +02:00
Diaoul b60cb8a81f pep8 for nautilus 2015-08-27 17:27:23 +02:00
Diaoul 835dcd6eec Add contributing notes for translations 2015-08-27 17:09:10 +02:00
Diaoul 26cb66aa45 Add a README to examples 2015-08-27 17:08:51 +02:00
Diaoul d4dc59aeba Update french translations 2015-08-27 17:06:50 +02:00
Diaoul 5258ef400f Code style 2015-08-27 17:06:13 +02:00
Diaoul 50ef6efcfa Discourage use of the single flag 2015-08-27 17:05:40 +02:00
Diaoul 4b1cb36343 Make sure dict keys are unique in nautilus 2015-08-27 11:59:46 +02:00
Diaoul 4324460005 Add nautilus integration 2015-08-27 00:03:48 +02:00
Diaoul 620688d92a Refactor paths in cli 2015-08-26 22:59:10 +02:00
Diaoul 89d5ea929b Add a config class in cli 2015-08-26 22:58:37 +02:00
Diaoul 168004dc89 Describe how to get a debug log file in CONTRIBUTING 2015-08-26 22:57:55 +02:00
Diaoul 1ad88032f2 Switch to dev version 2015-08-26 22:49:56 +02:00
Diaoul 4000f9e894 Fix allow_failures in travis 2015-08-24 15:17:00 +02:00
Diaoul 8b825263d4 Use use the VCR_RECORD_MODE environment variable in napiprojekt tests 2015-08-23 11:43:22 +02:00
Diaoul 7247568d96 Make sure extra pytest opts are passed to the test command 2015-08-23 11:37:09 +02:00
Diaoul 74d5175152 Fix travis matrix 2015-08-23 11:37:09 +02:00
Diaoul 130d961e72 Update tests to use the VCR_RECORD_MODE environment variable 2015-08-23 11:37:09 +02:00
Diaoul 02dd9a77f0 Add a travis configuration for integration tests without cassette 2015-08-23 11:37:09 +02:00
Diaoul f6f40d81a1 Use explicit strings for python versions in travis 2015-08-23 11:37:09 +02:00
Diaoul 79fb0442dd Remove unicode_literals imports 2015-08-23 11:36:32 +02:00
Diaoul 56a8b2c7cf Code style changes and slight refactoring of napiprojekt 2015-08-22 23:20:30 +02:00
Diaoul 71a06d5bbc Normalize some logging messages 2015-08-22 21:41:24 +02:00
Diaoul 55ac0a941b Merge remote-tracking branch 'bogdal/napiprojekt' into develop 2015-08-22 20:51:45 +02:00
Antoine Bertin 0aaa4347f8 Merge pull request #491 from h3llrais3r/develop
Fix logging error
2015-08-22 20:46:32 +02:00
Antoine Bertin 4c11e2d5c1 Merge pull request #492 from h3llrais3r/login-opensubtitles
Login opensubtitles
2015-08-22 20:46:10 +02:00
Diaoul 3bc239ffee Add contributing notes 2015-08-22 20:33:04 +02:00
h3llrais3r e797516468 Updated opensubtitles test for login bad password 2015-08-22 11:36:07 +02:00
h3llrais3r aa67ccd098 Clear token in opensubtitles terminate 2015-08-22 11:35:58 +02:00
h3llrais3r b1c5ca5a00 Cleanup and code formatting 2015-08-22 11:35:53 +02:00
h3llrais3r 050d29a186 Add tests for opensubtitles login 2015-08-22 11:35:50 +02:00
h3llrais3r 7d8ce8bb30 Add support for opensubitles login
Make it possible to specify your own username and password for
downloading from opensubtitles.
2015-08-22 11:35:44 +02:00
h3llrais3r 38f5d303f9 Fix logging error
Fix console error when logging score below min_score
2015-08-22 11:20:47 +02:00
Diaoul 38c86b4cf1 Fix library usage example in README 2015-08-21 17:52:01 +02:00
Adam Bogdał 1da6d181d5 Add NapiProjekt provider 2015-08-18 03:55:56 +02:00
Diaoul e0788be5af Release 1.0.1 2015-07-23 00:44:56 +02:00
Diaoul 52709a3e25 Remove useless dash in cli message 2015-07-23 00:35:05 +02:00
Diaoul 25cf0ac996 Fix scaled score in cli 2015-07-23 00:33:58 +02:00
Diaoul 4c3d7d5b9d Improve CLI
- Convert paths to str
- Add logs
- Catch errors
- Colored output report
2015-07-23 00:11:29 +02:00
Diaoul b4cbfc0de2 Add CLI api doc 2015-07-23 00:05:47 +02:00
Diaoul 600393c12a Fix scan_videos docstring 2015-07-22 19:43:34 +02:00
Diaoul faeeeca39a Switch to dev version 2015-07-22 19:43:07 +02:00
Diaoul 537d33b66a Release 1.0 2015-07-22 01:45:23 +02:00
Diaoul 3933db4936 Add wheel dev dependency 2015-07-22 01:45:12 +02:00
Diaoul 86c6d2c253 Add minimum required version for dependencies 2015-07-21 23:54:00 +02:00
Diaoul 75ffe3767f Skip doctests for python 2 2015-07-21 21:31:10 +02:00
Diaoul 015b7547db Use a session fixture to configure cache region 2015-07-21 21:30:48 +02:00
Diaoul 2a6d861968 Check for files in mkv fixture 2015-07-21 21:29:58 +02:00
Diaoul 0f188e30b0 Update badges and urls 2015-07-21 21:22:52 +02:00
Diaoul 5faf8277f1 Remove pip logs from travis cache 2015-07-21 02:20:31 +02:00
Diaoul b86b4f1405 Add mkv test data to travis cache 2015-07-21 02:09:30 +02:00
Diaoul 98f903174a Merge branch 'master' of github.com:Diaoul/subliminal 2015-07-21 01:56:52 +02:00
Diaoul d8df8a4624 Fix region configuration in tests 2015-07-21 01:56:40 +02:00
Diaoul 0df19e300f Remove --doctest-modules 2015-07-21 01:56:31 +02:00
Antoine Bertin b26bf1c470 Add License badge to README 2015-07-20 23:16:32 +02:00
Diaoul 53cd3f6118 Update documentation and add doctests 2015-07-20 23:00:55 +02:00
Diaoul c23f955b6c Merge branch 'master' of github.com:Diaoul/subliminal 2015-07-20 22:59:04 +02:00
Diaoul 5c5b2656ec Add possible values for min-score in cli help 2015-07-20 22:58:56 +02:00
Diaoul c6278909ff Remove redundant documentation 2015-07-20 22:54:09 +02:00
Diaoul 64d599d6f7 Add compute_score to subliminal namespace 2015-07-20 22:53:40 +02:00
Diaoul 90d638e9c6 Replace DispatchExtensionManager with EnabledExtensionManager 2015-07-20 22:53:13 +02:00
Antoine Bertin 1dabbf78b2 Simplify example in README 2015-07-16 16:18:10 +02:00
Antoine Bertin 58b0b0c756 Improve README readability 2015-07-16 16:13:49 +02:00
Antoine Bertin 1426d9d62d Add more verbosity for travis 2015-07-16 15:58:56 +02:00
Antoine Bertin 85db6c7884 Merge pull request #463 from martinp/python2-fix
Fix for Python 2
2015-07-16 15:30:26 +02:00
Martin Polden 43d02c349f Fix for Python 2
Python 2 doesn't have the exist_ok parameter.
2015-07-16 13:26:55 +02:00
Diaoul c7af4573ca Add cache and sudo directives to travis 2015-07-16 01:41:45 +02:00
Diaoul 73a217eb1b Fix travis build 2015-07-16 01:14:33 +02:00
Diaoul b1854222a0 Merge branch 'master' of github.com:Diaoul/subliminal 2015-07-16 01:05:09 +02:00
Diaoul 7cb72b2bb2 Update to 1.0.dev0 2015-07-16 01:01:38 +02:00
Antoine Bertin 00245dd44d Merge pull request #451 from h3llrais3r/fix_podnapisi_provider
Fix podnapisi provider
2015-05-09 19:22:30 +02:00
h3llrais3r c1972ab26f Fix podnapisi provider
The download url is not longer a link on the page, but it's the
subtitle.page_link appended with '/download'.
2015-05-09 17:59:40 +02:00
Antoine Bertin b86254b0b8 Merge pull request #426 from oxan/addic7ed-codec
Also guess video codec from version in addic7ed provider
2015-01-31 17:51:59 +01:00
Oxan van Leeuwen 00157ee655 Also guess video codec from version in addic7ed provider 2014-12-27 21:17:22 +01:00
Antoine Bertin 6aaece1c44 Merge pull request #403 from h3llrais3r/opensubtitles-query-search
Implemented query search for opensubtitles
2014-09-17 20:27:45 +02:00
h3llrais3r 6445064836 Implemented query search for opensubtitles
Opensubtitles was only using the hash search functionality. For series,
it will now search on series name, season and episode.
2014-09-17 10:17:45 +02:00
Antoine Bertin 9d0c9d93a0 Merge pull request #396 from joseflavio/master
Adding support to proper decode Polish and Bulgarian
2014-08-17 20:09:31 +02:00
Jose Flavio Aguilar 01a06166ff Adding support to proper decode Polish and Bulgarian 2014-08-17 18:49:19 +02:00
Antoine Bertin f41fc2bceb Merge pull request #377 from goll/master
Update for python-guessit 0.7.1
2014-05-07 18:24:10 +02:00
Adrian Goll 5ffa344f1c Update for python-guessit 0.7.1
Latest guessit doesn't require 'autodetect' anymore. Here is a snippet from the usage:

-t TYPE, --type=TYPE  the suggested file type: movie, episode. If undefined, type will be guessed.
2014-05-07 14:18:14 +02:00
Antoine Bertin 31c2b21350 Merge pull request #362 from northerndrifter/p1
Fix hearing impaired flag for podnapisi
2014-03-26 10:36:13 +01:00
northerndrifter b2c6562c64 fixed hearing impaired flag for podnapisi 2014-03-25 21:01:13 +01:00
Antoine Bertin 02a36d9cad Merge pull request #336 from Nikoli/master
Use pysrt 1.0.1: first version migrated from charade to chardet
2014-02-17 10:32:05 +01:00
Nikoli 5dcb1916b2 Use pysrt 1.0.1: first version migrated from charade to chardet 2014-02-14 04:41:09 +04:00
Antoine Bertin c5623ec868 Merge pull request #327 from queeup/master
Fix wrong Turkish subtitle encoding detection #315
2014-02-12 14:58:07 +01:00
queeup 1c17c1987f Fix wrong Turkish subtitle encoding detection #315
Because of chardet Turkish encoding detect issue subliminal can't
recognize Turkish encoding and convert correctly to utf-8 #315
2014-02-12 01:28:57 +02:00
Antoine Bertin b6318bfae0 Update docs and HISTORY 2014-02-11 23:23:00 +01:00
Antoine Bertin 0e8489951d Use bytes for subtitle's content and refactor subtitle validation 2014-02-11 23:22:47 +01:00
Antoine Bertin 21d3a1c1bb Merge pull request #331 from h3llrais3r/master
Add a format to Video and use it for matching
2014-02-09 13:44:36 +01:00
h3llrais3r 9055021282 Added format to hash equation 2014-02-08 23:41:12 +01:00
h3llrais3r 10fcfd21b1 Ignore Pycharm ide files 2014-02-08 22:37:22 +01:00
h3llrais3r c142233ff9 Upgraded guessit to version 0.7 2014-02-08 20:28:30 +01:00
h3llrais3r e660e47265 Fixed unit tests related to format matching (2) 2014-02-07 23:22:36 +01:00
h3llrais3r 40c4c0aeec Fixed unit tests related to format matching 2014-02-07 22:36:44 +01:00
h3llrais3r 97c9a7b025 Updated methods for guess properties 2014-02-07 22:34:18 +01:00
h3llrais3r 9145f2530b Added format for matching on movie 2014-02-07 22:33:37 +01:00
h3llrais3r a1b073e5d6 Fixed guess properties for tvsubtitles 2014-02-07 22:32:03 +01:00
h3llrais3r 7425153760 Use guessit method for partial filenames
Use the correct guessit method when we don't have the complete filename.
We don't fake an episode filename anymore.
2014-02-05 22:37:07 +01:00
h3llrais3r c0836a94a9 Use guessit for matching 2014-01-30 23:11:29 +01:00
h3llrais3r 9dc24951a2 Use format for matching 2014-01-30 21:32:55 +01:00
Antoine Bertin f3eaad8d1c Close xmlrpc in terminate in opensubtitles 2014-01-29 00:17:22 +01:00
Antoine Bertin effde5014e Use little-endian for OpenSubtitles hash 2014-01-28 23:43:35 +01:00
Antoine Bertin 74c4c06a5c Fix cli with new ProviderManager 2014-01-28 21:52:11 +01:00
Antoine Bertin f10b7683c9 Rework provider loading and management
- Rename ProviderManager to ProviderPool
- Add a ProviderManager to manage the entry point
2014-01-27 22:17:00 +01:00
Antoine Bertin 5e26185c9f Fixes for babelfish 0.5.1 2014-01-27 20:58:31 +01:00
Antoine Bertin 2d350f5340 Merge branch 'master' of github.com:Diaoul/subliminal 2014-01-26 14:29:32 +01:00
Antoine Bertin f0519bbefb Update for babelfish 0.5.1 2014-01-26 14:29:20 +01:00
Antoine Bertin 7acfb3f027 More python3 compatibility 2014-01-26 14:27:54 +01:00
Antoine Bertin 24621e15e3 Merge pull request #317 from doron1/patch-1
Update subtitle.py
2014-01-21 13:45:34 -08:00
doron1 3474b2363f Update subtitle.py 2014-01-21 20:45:44 +02:00
Antoine Bertin 560dea3e3e More python3 compatibility 2014-01-19 18:48:50 +01:00
Antoine Bertin f6e5cf91ab Use pysrt 1.0.0 2014-01-19 14:55:19 +01:00
Antoine Bertin 57e8770fda Update HISTORY 2014-01-19 14:45:49 +01:00
Antoine Bertin 7e8f7e41b5 Update unittests 2014-01-19 14:36:37 +01:00
Antoine Bertin 84d890d7b0 Refactor exceptions, add a TimeoutTransport and fix line endings 2014-01-19 14:36:26 +01:00
Antoine Bertin e57c90b97e Respect xdg for cache file 2014-01-19 09:46:00 +01:00
Antoine Bertin 236c43b807 Improve version numbering in documentation 2013-12-13 14:29:58 +01:00
Antoine Bertin bb32c286d9 Fix podnapisi provider 2013-12-12 23:42:37 +01:00
Antoine Bertin f1d4975079 Fix a typo in documentation examples 2013-12-12 22:11:27 +01:00
Antoine Bertin 464b783477 Fix release detection in podnapisi 2013-12-05 20:23:41 +01:00
Antoine Bertin c4756030c7 Fix subtitle re download with single 2013-12-05 20:20:23 +01:00
Antoine Bertin bf538fee32 Add support for directory and encoding in cli 2013-12-03 21:41:20 +01:00
Antoine Bertin cc32c29930 Use debug level for language parsing error in subtitle track name 2013-12-03 21:41:04 +01:00
Antoine Bertin 27b8703949 Add asctime to log file in cli 2013-12-03 21:40:21 +01:00
Antoine Bertin 0d9bbff534 Rename folder_path to directory and add encoding in save_subtitles 2013-12-03 21:39:37 +01:00
Antoine Bertin cad60e73a6 Fix single subtitles saving in cli 2013-12-03 20:36:01 +01:00
Antoine Bertin 1d14d21684 Update addic7ed unittests 2013-12-02 21:10:56 +01:00
Antoine Bertin 0733ef7d32 Fix podnapisi download 2013-12-02 20:29:45 +01:00
Antoine Bertin 143f872166 Remove debug print statement in decode 2013-12-02 20:29:30 +01:00
Antoine Bertin 95abab3c18 Catch more video age detection errors 2013-12-02 20:23:31 +01:00
Antoine Bertin 2e5fb46ebc Fix example in documentation 2013-12-02 00:27:59 +01:00
Antoine Bertin 0d11092178 Improve encoding detection 2013-12-01 22:00:31 +01:00
Antoine Bertin 80589f325a Use lowercase comments 2013-12-01 21:57:02 +01:00
Antoine Bertin 0b6b3d0905 Fix relative links in addic7ed and tvsubtitles for page_link 2013-12-01 18:16:14 +01:00
Antoine Bertin bc97f772b8 Remove bierdopje from the docs 2013-12-01 18:15:30 +01:00
Antoine Bertin efe944a10c Remove extra end of line character in cli 2013-12-01 10:17:57 +01:00
Antoine Bertin 9d9aed2d4b Add a page_link attribute to Subtitle 2013-11-30 23:42:21 +01:00
Antoine Bertin 9cd8b7d593 Use print statement to write to stderr 2013-11-30 23:41:24 +01:00
Antoine Bertin abfd2361d4 Use Video.fromname in cli 2013-11-30 23:41:07 +01:00
Antoine Bertin b10e616ec2 Add traceback to enzyme parsing error log 2013-11-28 21:26:22 +01:00
Antoine Bertin 7dc2a90edc Catch video age detection errors 2013-11-28 21:22:54 +01:00
Antoine Bertin cb53199748 Update cli documentation 2013-11-28 20:48:49 +01:00
Antoine Bertin 7b2402c436 More explicit log messages in api 2013-11-28 20:21:37 +01:00
Antoine Bertin 0715437888 Add support for file logging in cli 2013-11-28 20:21:21 +01:00
Antoine Bertin bea95113e7 Update HISTORY 2013-11-28 00:47:57 +01:00
Antoine Bertin e73e969f58 Remove dead BierDopje provider 2013-11-28 00:47:47 +01:00
Antoine Bertin fd30cf7388 Add year in episode
Used to make a difference between two series with the same name
2013-11-28 00:46:37 +01:00
Antoine Bertin fa9792b280 Fix missing return statement for Video.fromname 2013-11-27 23:33:37 +01:00
Antoine Bertin e93530c7c7 Add a maximum expiration time to cached functions 2013-11-25 23:25:31 +01:00
Antoine Bertin 35d4c37d61 Update to new API
- Add some provider utilities
- Add a ProviderManager class to manage multiple providers
- Add a content attribute to the Subtitle class
- Dissociate download and save functions to give more control to the
user
- Add a fromname classmethod to Video, Episode and Movie classes
- Update unittests
- Update documentation
2013-11-25 22:26:11 +01:00
Antoine Bertin 4d61d3fc42 Switch to 0.8.0 2013-11-22 23:44:02 +01:00
Antoine Bertin a24388137e Remove sphinxcontrib-programoutput 2013-11-22 23:38:17 +01:00
Antoine Bertin 60c7666610 Update badges in README 2013-11-22 21:38:12 +01:00
Antoine Bertin e11f1c4b28 Fix unittests for addic7ed 2013-11-22 21:31:12 +01:00
Antoine Bertin e1b32f237c Merge branch 'develop' 2013-11-22 21:06:04 +01:00
Antoine Bertin fe76634d02 Release 0.7.3 2013-11-22 21:05:49 +01:00
Antoine Bertin b499540bed Sync README and documentation 2013-11-22 20:47:56 +01:00
Antoine Bertin 7b4a9c2060 Fix wrong error catched for babelfish 0.4.0 2013-11-21 23:52:06 +01:00
Antoine Bertin a84cc80a88 Ignore IDE error in cli 2013-11-21 23:51:25 +01:00
Antoine Bertin 241cea9729 Fix podnapisi tests 2013-11-21 23:31:43 +01:00
Antoine Bertin 4b83ddc63e Improve assertions in tests 2013-11-21 23:31:02 +01:00
Antoine Bertin 0b431fbb8d Add Podnapisi to the list of providers in documentation 2013-11-21 00:38:01 +01:00
Antoine Bertin 3736d921a1 Update dogpile.cache to 0.5.2 and use a MutexLock in cli 2013-11-21 00:12:53 +01:00
Antoine Bertin 380fb28d2e Update to babelfish 0.4.0 2013-11-20 22:40:06 +01:00
Antoine Bertin 64c0ee4ccf Add setuptools to dev-requirements.txt 2013-11-20 22:38:55 +01:00
Antoine Bertin 5977bf69fb Improve embedded subtitles language detection 2013-11-14 22:08:48 +01:00
CelestianX 179ae6a24e Updated README with proper information
Section relative to the library was invalid. Missing references and
arguments.
2013-11-14 21:55:24 +01:00
Antoine Bertin 16942ec4c7 Switch to 0.7.3 2013-11-14 21:53:36 +01:00
Antoine Bertin 6f5378ea40 Be more permissive in subtitle validation 2013-11-14 21:51:39 +01:00
Antoine Bertin 02ee2039f4 Skip empty language in addic7ed 2013-11-14 21:51:04 +01:00
Antoine Bertin 60610e2032 Merge branch 'develop'
Conflicts:
	requirements.txt
	setup.py
	subliminal/infos.py
2013-10-29 12:43:20 +01:00
Antoine Bertin 277b046b41 Fix requirements for enzyme 0.3 2013-05-19 15:44:49 +02:00
Antoine Bertin c823eda245 Update NEWS 2013-01-17 21:09:28 +01:00
Antoine Bertin 6340de0ddb Fix requirements due to requests 1.0 2013-01-17 20:49:41 +01:00
235 changed files with 104628 additions and 2602 deletions
+4 -1
View File
@@ -1,5 +1,8 @@
[report]
exclude_lines =
def __repr__
pragma: no cover
raise NotImplementedError
def __repr__
if __name__ == .__main__.:
omit =
subliminal/cli.py
+48 -27
View File
@@ -1,43 +1,64 @@
*.py[co]
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.tox
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.mo
*.pot
# Mr Developer
.mr.developer.cfg
# Django stuff:
*.log
# Pydev
.project
.pydevproject
.settings
# Sphinx documentation
docs/_build/
# Rope
.ropeproject
# PyBuilder
target/
# Sphinx
docs/_build
# Pycharm
.idea
# Subliminal unittests
subliminal/tests/*.srt
subliminal/tests/*_files
subliminal/tests/*_cache
# Subliminal
tests/data/mkv/
-3
View File
@@ -1,3 +0,0 @@
[submodule "docs/_themes"]
path = docs/_themes
url = git://github.com/Diaoul/diaoul-sphinx-themes.git
+41 -16
View File
@@ -1,24 +1,49 @@
sudo: false
language: python
python:
- "2.7"
- "3.3"
- "3.4"
- "3.5"
env:
- PARSER=native
- PARSER=lxml
addons:
apt:
packages:
- unrar
matrix:
include:
- python: "3.5"
env:
- PARSER=native
- VCR_RECORD_MODE=all
- PYTEST_ADDOPTS="-m integration"
allow_failures:
- python: "3.5"
env:
- PARSER=native
- VCR_RECORD_MODE=all
- PYTEST_ADDOPTS="-m integration"
cache:
directories:
- $HOME/.cache/pip
- tests/data/mkv
before_cache:
- rm -f $HOME/.cache/pip/log/debug.log
install:
- pip install coveralls --use-mirrors
- pip install -r requirements.txt --use-mirrors
- pip install -e .[test]
- if [ $PARSER = "lxml" ]; then pip install lxml; fi
- pip install coveralls
script:
- coverage run --source=subliminal setup.py test
script: python setup.py test --addopts "--cov subliminal --verbose $PYTEST_ADDOPTS"
after_success:
- coveralls
notifications:
email: false
irc:
channels:
- "irc.freenode.org#subliminal"
on_success: change
on_failure: always
use_notice: true
skip_join: true
after_success: coveralls
+19
View File
@@ -0,0 +1,19 @@
Contributing
============
Issues
------
Issues are intended for bug report and feature requests. For any bug report please make sure to include the complete
stack trace and DEBUG level logs as well as reproduce steps.
If you use the CLI, you can create a debug log file with `subliminal --debug [...] 2> debug.log`.
Pull Requests
-------------
You can contribute code and documentation with pull requests. Any code contribution must be unit tested and the pull
request open against the *develop* branch.
Translations
------------
Contribution to translations can be made on [subliminal's transifex page](https://www.transifex.com/subliminal/subliminal/)
Subliminal is configured to work with [transifex-client](http://docs.transifex.com/client/)
+146 -14
View File
@@ -1,8 +1,117 @@
Changelog
=========
---------
2.0.3
^^^^^
**release date:** 2016-06-10
* Fix clearing cache in CLI
2.0.2
^^^^^
**release date:** 2016-06-06
* Fix for dogpile.cache>=0.6.0
* Fix missing sphinx_rtd_theme dependency
2.0.1
^^^^^
**release date:** 2016-06-06
* Fix beautifulsoup4 minimal requirement
2.0.0
^^^^^
**release date:** 2016-06-04
* Add refiners to enrich videos with information from metadata, tvdb and omdb
* Add asynchronous provider search for faster searches
* Add registrable managers so subliminal can run without install
* Add archive support
* Add the ability to customize scoring logic
* Add an age argument to scan_videos for faster scanning
* Add legendas.tv provider
* Add shooter.cn provider
* Improve matching and scoring
* Improve documentation
* Split nautilus integration into its own project
1.1.1
^^^^^
**release date:** 2016-01-03
* Fix scanning videos on bad MKV files
1.1
^^^
**release date:** 2015-12-29
* Fix library usage example in README
* Fix for series name with special characters in addic7ed provider
* Fix id property in thesubdb provider
* Improve matching on titles
* Add support for nautilus context menu with translations
* Add support for searching subtitles in a separate directory
* Add subscenter provider
* Add support for python 3.5
1.0.1
^^^^^
**release date:** 2015-07-23
* Fix unicode issues in CLI (python 2 only)
* Fix score scaling in CLI (python 2 only)
* Improve error handling in CLI
* Color collect report in CLI
1.0
^^^
**release date:** 2015-07-22
* Many changes and fixes
* New test suite
* New documentation
* New CLI
* Added support for SubsCenter
0.7.5
^^^^^
**release date:** 2015-03-04
* Update requirements
* Remove BierDopje provider
* Add pre-guessed video optional argument in scan_video
* Improve hearing impaired support
* Fix TVSubtitles and Podnapisi providers
0.7.4
^^^^^
**release date:** 2014-01-27
* Fix requirements for guessit and babelfish
0.7.3
^^^^^
**release date:** 2013-11-22
* Fix windows compatibility
* Improve subtitle validation
* Improve embedded subtitle languages detection
* Improve unittests
0.7.2
-----
^^^^^
**release date:** 2013-11-10
* Fix TVSubtitles for ambiguous series
@@ -15,7 +124,7 @@ Changelog
0.7.1
-----
^^^^^
**release date:** 2013-11-06
* Improve CLI
@@ -25,7 +134,7 @@ Changelog
0.7.0
-----
^^^^^
**release date:** 2013-10-29
**WARNING:** Complete rewrite of subliminal with backward incompatible changes
@@ -42,8 +151,23 @@ Changelog
* Drop a few providers
* And much more...
0.6.4
^^^^^
**release date:** 2013-05-19
* Fix requirements due to enzyme 0.3
0.6.3
^^^^^
**release date:** 2013-01-17
* Fix requirements due to requests 1.0
0.6.2
-----
^^^^^
**release date:** 2012-09-15
* Fix BierDopje
@@ -54,8 +178,9 @@ Changelog
* Add possible services in help message of the CLI
* Allow existing filenames to be passed without the ./ prefix
0.6.1
-----
^^^^^
**release date:** 2012-06-24
* Fix subtitle release name in BierDopje
@@ -72,9 +197,11 @@ Changelog
* Fix guessit.Language in Video.scan
* Fix language detection of subtitles
0.6.0
-----
^^^^^
**release date:** 2012-06-16
**WARNING:** Backward incompatible changes
* Fix --workers option in CLI
@@ -88,13 +215,14 @@ Changelog
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
@@ -107,15 +235,17 @@ Changelog
* 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
@@ -125,16 +255,18 @@ Changelog
* 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
^^^
**release date:** *private release*
* Initial release
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2013 Antoine Bertin
Copyright (c) 2016 Antoine Bertin
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
+58 -32
View File
@@ -1,56 +1,82 @@
Subliminal
==========
Subtitles, faster than your thoughts.
Subliminal is a python library to search and download subtitles.
It comes with an easy to use CLI (command-line interface) suitable for direct use or cron jobs.
.. image:: https://img.shields.io/pypi/v/subliminal.svg
:target: https://pypi.python.org/pypi/subliminal
:alt: Latest Version
.. image:: https://travis-ci.org/Diaoul/subliminal.png?branch=develop
:target: https://travis-ci.org/Diaoul/subliminal
.. image:: https://travis-ci.org/Diaoul/subliminal.svg?branch=develop
:target: https://travis-ci.org/Diaoul/subliminal
:alt: Travis CI build status
.. image:: https://coveralls.io/repos/Diaoul/subliminal/badge.png?branch=develop
:target: https://coveralls.io/r/Diaoul/subliminal?branch=develop
.. image:: https://readthedocs.org/projects/subliminal/badge/?version=latest
:target: https://subliminal.readthedocs.org/
:alt: Documentation Status
.. image:: https://coveralls.io/repos/Diaoul/subliminal/badge.svg?branch=develop&service=github
:target: https://coveralls.io/github/Diaoul/subliminal?branch=develop
:alt: Code coverage
.. image:: https://img.shields.io/github/license/Diaoul/subliminal.svg
:target: https://github.com/Diaoul/subliminal/blob/master/LICENSE
:alt: License
.. image:: https://img.shields.io/badge/gitter-join%20chat-1dce73.svg
:alt: Join the chat at https://gitter.im/Diaoul/subliminal
:target: https://gitter.im/Diaoul/subliminal
Providers
---------
Subliminal uses multiple providers to give users a vast choice and have a better chance to find the
best matching subtitles. Providers are extensible through a dedicated entry point.
* Addic7ed
* BierDopje
* OpenSubtitles
* TheSubDB
* TvSubtitles
:Project page: https://github.com/Diaoul/subliminal
:Documentation: https://subliminal.readthedocs.org/
Usage
-----
CLI
^^^
Download english subtitles::
Download English subtitles::
$ subliminal -l en -- The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
1 subtitle downloaded
$ subliminal download -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
Collecting videos [####################################] 100%
1 video collected / 0 video ignored / 0 error
Downloading subtitles [####################################] 100%
Downloaded 1 subtitle
Library
^^^^^^^
Download best subtitles in French and English for videos less than one week old in a video folder,
skipping videos that already have subtitles whether they are embedded or not::
Download best subtitles in French and English for videos less than two weeks old in a video folder:
.. code:: python
from datetime import timedelta
from babelfish import Language
from datetime import timedelta
import subliminal
from subliminal import download_best_subtitles, region, save_subtitles, scan_videos
# configure the cache
subliminal.cache_region.configure('dogpile.cache.dbm', arguments={'filename': '/path/to/cachefile.dbm'})
region.configure('dogpile.cache.dbm', arguments={'filename': 'cachefile.dbm'})
# scan for videos in the folder and their subtitles
videos = scan_videos(['/path/to/video/folder'], subtitles=True, embedded_subtitles=True)
# scan for videos newer than 2 weeks and their existing subtitles in a folder
videos = scan_videos('/video/folder', age=timedelta(weeks=2))
# download
subliminal.download_best_subtitles(videos, {Language('eng'), Language('fra')}, age=timedelta(week=1))
# download best subtitles
subtitles = download_best_subtitles(videos, {Language('eng'), Language('fra')})
# save them to disk, next to the video
for v in videos:
save_subtitles(v, subtitles[v])
License
-------
MIT
Installation
------------
Subliminal can be installed as a regular python module by running::
$ [sudo] pip install subliminal
For a better isolation with your system you should use a dedicated virtualenv or install for your user only using
the ``--user`` flag.
Nautilus/Nemo integration
-------------------------
See the dedicated `project page <https://github.com/Diaoul/nautilus-subliminal>`_ for more information.
+1 -4
View File
@@ -1,4 +1 @@
sympy>=0.7.3
sphinx>=1.1.3
sphinxcontrib-programoutput>=0.8
Sphinx-PyPI-upload>=0.2.1
-e .[dev,test]
+17 -2
View File
@@ -2,7 +2,7 @@
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXOPTS = -n -W
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
@@ -19,7 +19,7 @@ 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
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@@ -30,6 +30,7 @@ help:
@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 " applehelp to make an Apple Help Book"
@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"
@@ -45,6 +46,7 @@ help:
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
@@ -89,6 +91,14 @@ qthelp:
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/subliminal.qhc"
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@@ -166,6 +176,11 @@ doctest:
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
-4
View File
@@ -1,4 +0,0 @@
<h3>Subliminal</h3>
<p>
Subliminal is a Python library to search and download subtitles.
</p>
Submodule docs/_themes deleted from 24aa9748e4
-8
View File
@@ -1,8 +0,0 @@
API
===
.. module:: subliminal.api
.. autodata:: PROVIDERS_ENTRY_POINT
.. autofunction:: list_subtitles
.. autofunction:: download_subtitles
.. autofunction:: download_best_subtitles
+14 -4
View File
@@ -2,10 +2,20 @@ Cache
=====
.. module:: subliminal.cache
.. autodata:: CACHE_VERSION
.. autofunction:: subliminal_key_generator
.. autodata:: SHOW_EXPIRATION_TIME
:annotation:
.. autodata:: EPISODE_EXPIRATION_TIME
:annotation:
.. autodata:: REFINER_EXPIRATION_TIME
:annotation:
.. data:: region
:annotation:
The dogpile.cache region
The :class:`~dogpile.cache.region.CacheRegion`
Refer to `dogpile.cache's documentation <http://dogpilecache.readthedocs.org>`_ to see how to configure the region
Refer to dogpile.cache's `region configuration documentation
<http://dogpilecache.readthedocs.org/en/latest/usage.html#region-configuration>`_ to see how to configure the region
+1 -5
View File
@@ -1,7 +1,3 @@
CLI
===
.. module:: subliminal.cli
subliminal
----------
.. program-output:: subliminal --help
.. automodule:: subliminal.cli
+7
View File
@@ -0,0 +1,7 @@
Core
====
.. automodule:: subliminal.core
:exclude-members: ARCHIVE_EXTENSIONS
.. autodata:: ARCHIVE_EXTENSIONS
:annotation:
+1 -7
View File
@@ -1,9 +1,3 @@
Exceptions
==========
.. module:: subliminal.exceptions
.. autoclass:: Error
.. autoclass:: ProviderError
.. autoclass:: ProviderConfigurationError
.. autoclass:: ProviderNotAvailable
.. autoclass:: InvalidSubtitle
.. automodule:: subliminal.exceptions
+3
View File
@@ -0,0 +1,3 @@
Extensions
==========
.. automodule:: subliminal.extensions
+45 -3
View File
@@ -1,6 +1,48 @@
Providers
=========
.. module:: subliminal.providers
.. automodule:: subliminal.providers
.. autoclass:: Provider
:members:
Addic7ed
--------
.. automodule:: subliminal.providers.addic7ed
:private-members:
LegendasTv
----------
.. automodule:: subliminal.providers.legendastv
:private-members:
NapiProjekt
-----------
.. automodule:: subliminal.providers.napiprojekt
:private-members:
OpenSubtitles
-------------
.. automodule:: subliminal.providers.opensubtitles
:private-members:
Podnapisi
---------
.. automodule:: subliminal.providers.podnapisi
:private-members:
Shooter
-------
.. automodule:: subliminal.providers.shooter
:private-members:
SubsCenter
----------
.. automodule:: subliminal.providers.subscenter
:private-members:
TheSubDB
--------
.. automodule:: subliminal.providers.thesubdb
:private-members:
TVsubtitles
-----------
.. automodule:: subliminal.providers.tvsubtitles
:private-members:
+20
View File
@@ -0,0 +1,20 @@
.. _refiners:
Refiners
========
.. automodule:: subliminal.refiners
Metadata
--------
.. autofunction:: subliminal.refiners.metadata.refine
TVDB
----
.. autofunction:: subliminal.refiners.tvdb.refine
OMDb
----
.. autofunction:: subliminal.refiners.omdb.refine
+1 -4
View File
@@ -1,6 +1,3 @@
Score
=====
.. module:: subliminal.score
.. autofunction:: get_episode_equations
.. autofunction:: get_movie_equations
.. automodule:: subliminal.score
+4 -6
View File
@@ -1,9 +1,7 @@
Subtitle
========
.. module:: subliminal.subtitle
.. automodule:: subliminal.subtitle
:exclude-members: SUBTITLE_EXTENSIONS
.. autoclass:: Subtitle
:members:
.. autofunction:: get_subtitle_path
.. autofunction:: is_valid_subtitle
.. autofunction:: compute_guess_matches
.. autodata:: SUBTITLE_EXTENSIONS
:annotation:
+3
View File
@@ -0,0 +1,3 @@
Utils
=====
.. automodule:: subliminal.utils
+4 -14
View File
@@ -1,17 +1,7 @@
Video
=====
.. module:: subliminal.video
.. automodule:: subliminal.video
:exclude-members: VIDEO_EXTENSIONS
.. autodata:: VIDEO_EXTENSIONS
.. autodata:: SUBTITLE_EXTENSIONS
.. autoclass:: Video
:members:
.. autoclass:: Episode
:members:
.. autoclass:: Movie
:members:
.. autofunction:: hash_opensubtitles
.. autofunction:: hash_thesubdb
.. autofunction:: scan_subtitle_languages
.. autofunction:: scan_video
.. autofunction:: scan_videos
.. autodata:: VIDEO_EXTENSIONS
:annotation:
+658
View File
@@ -0,0 +1,658 @@
interactions:
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1
response:
body: {string: "<html>\r\n<head><title>302 Found</title></head>\r\n<body bgcolor=\"\
white\">\r\n<center><h1>302 Found</h1></center>\r\n<hr><center>nginx/1.8.0</center>\r\
\n</body>\r\n</html>\r\n"}
headers:
Connection: [keep-alive]
Content-Length: ['160']
Content-Type: [text/html]
Date: ['Wed, 23 Mar 2016 09:34:06 GMT']
Location: ['http://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1']
Server: [nginx/1.8.0]
status: {code: 302, message: Moved Temporarily}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1
response:
body: {string: "<html>\r\n<head><title>301 Moved Permanently</title></head>\r\n\
<body bgcolor=\"white\">\r\n<center><h1>301 Moved Permanently</h1></center>\r\
\n<hr><center>nginx/1.8.0</center>\r\n</body>\r\n</html>\r\n"}
headers:
Connection: [keep-alive]
Content-Length: ['184']
Content-Type: [text/html]
Date: ['Wed, 23 Mar 2016 09:34:07 GMT']
Location: ['https://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1']
Server: [nginx/1.8.0]
status: {code: 301, message: Moved Permanently}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: https://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1
response:
body:
string: !!binary |
H4sIAAAAAAAAA81VWW/bRhB+z6/YKC9OIXJJHT6C8QZKpNYCHDmFhbZJUARrcU0uSi0JHpIN5Md3
hjecZNu+FHmRZma/2Tn2myG8ftjH7KCyXCfmcuS73ogps0sCbcLLUVncO+cjlhfSBDJOjLocPap8
9Fo8g+fLm7fbD+9XLFN5GRc5+/SMMXi+ul69W222nfXkxfu3y8V28SWVoTaywChf8vKu0EWsXv4k
0Gfo1YPYya7MMmWKMdslJf01cV7WPkOvBsnaWN+C0B1PAX22bUbsRAdjViU3Zo9KZmO2Tw5ardFa
ZjH+pHEiA5VVhkbeyL2i9GIlcxRiacJShpVPK9eQQhOwONwiMDEkrVKdJ0FtTNVOSwyxC/Ixu0+y
vcSi71NUMmybCVGJZYhqkBwNZZF/o04dPC1y2KjUflzVbfOnjtjOm17ZINhF63HX33+DorbacM2b
2CDtC62tjWtR/xSwxdkiEgus5w0/7JiGOXbQbc0pGwjZZjuueWhFpNYLau5aLyBW2wBGHT9X1P8a
NCQ3Qb5G9DPejY0NpB7krriSeaSGKf0pGDTrR0C/owQ0m0f4wFsRqnVVWWjniM5xAry7gw8vaZeP
AB0If3Yx92eIRRlwXsXHYvE7cJKgmk+xjRR7o0P2BvcMQyXJHoHXR0ADKiaedwa8EqGZSJF/8H4B
3mqAUyiiokhfcX48Ht00CYzEVaRdowre5pPzqHSKSDl3OnTuMBopGM2hAM6tN1/557xOj+6DfjeK
mefPT4EPLN0pjZD465DFqj+vbNioaoNSgS4W6FKBbl2gW0dzr5bb35zrm2uq+3uYs4mXVkD3YXI6
c5Zr/B7drm821P46ALRjusZ+z4EP1O6oSumqNKHMtDQ9pk619RBR2R/RA2Ft/nTqTfDn9IKeBQ3Q
bnyBsToZrc0MC/+c7K1G8HpuhVfhGwVoVLmAeiTFhi+ANzLgZ6IxoAD1zAnPRf9GhmqARIQuNG4C
uqHCC9EiIh1GnwN1r42mb3SNE8AHuG6AxPSCUu51GI4NpthRSEBLpo7c3gUSg9hMlL7Z/NDkrtN7
Sm4c9u8Se2ES87hPyrzH/Cdyu38c9NL5+dd3/ydb/elsfkbc/LHYSmTv6EdKT7jpHB+hVy3863bu
37blw93pCgAA
headers:
Connection: [keep-alive]
Content-Encoding: [gzip]
Content-Type: [text/xml;charset=utf-8]
Date: ['Wed, 23 Mar 2016 09:34:07 GMT']
Server: [nginx/1.8.0]
Vary: [Accept-Encoding]
status: {code: 200, message: OK}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://podnapisi.net/subtitles/ZtAW/download?container=zip
response:
body: {string: "<html>\r\n<head><title>302 Found</title></head>\r\n<body bgcolor=\"\
white\">\r\n<center><h1>302 Found</h1></center>\r\n<hr><center>nginx/1.8.0</center>\r\
\n</body>\r\n</html>\r\n"}
headers:
Connection: [keep-alive]
Content-Length: ['160']
Content-Type: [text/html]
Date: ['Wed, 23 Mar 2016 09:34:07 GMT']
Location: ['http://www.podnapisi.net/subtitles/ZtAW/download?container=zip']
Server: [nginx/1.8.0]
status: {code: 302, message: Moved Temporarily}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://www.podnapisi.net/subtitles/ZtAW/download?container=zip
response:
body: {string: "<html>\r\n<head><title>301 Moved Permanently</title></head>\r\n\
<body bgcolor=\"white\">\r\n<center><h1>301 Moved Permanently</h1></center>\r\
\n<hr><center>nginx/1.8.0</center>\r\n</body>\r\n</html>\r\n"}
headers:
Connection: [keep-alive]
Content-Length: ['184']
Content-Type: [text/html]
Date: ['Wed, 23 Mar 2016 09:34:07 GMT']
Location: ['https://www.podnapisi.net/subtitles/ZtAW/download?container=zip']
Server: [nginx/1.8.0]
status: {code: 301, message: Moved Permanently}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: https://www.podnapisi.net/subtitles/ZtAW/download?container=zip
response:
body:
string: !!binary |
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
headers:
Accept-Ranges: [bytes]
Connection: [keep-alive]
Content-Disposition: [attachment; filename="e638ea178f406cb584f48051501e2cb4db4fce1d.zip"]
Content-Length: ['10541']
Content-Type: [application/octet-stream]
Date: ['Wed, 23 Mar 2016 09:34:07 GMT']
ETag: ['"4f497ffb-292d"']
Last-Modified: ['Sun, 26 Feb 2012 00:42:35 GMT']
Server: [nginx/1.8.0]
status: {code: 200, message: OK}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1
response:
body: {string: "<html>\r\n<head><title>302 Found</title></head>\r\n<body bgcolor=\"\
white\">\r\n<center><h1>302 Found</h1></center>\r\n<hr><center>nginx/1.8.0</center>\r\
\n</body>\r\n</html>\r\n"}
headers:
Connection: [keep-alive]
Content-Length: ['160']
Content-Type: [text/html]
Date: ['Wed, 23 Mar 2016 09:34:07 GMT']
Location: ['http://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1']
Server: [nginx/1.8.0]
status: {code: 302, message: Moved Temporarily}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1
response:
body: {string: "<html>\r\n<head><title>301 Moved Permanently</title></head>\r\n\
<body bgcolor=\"white\">\r\n<center><h1>301 Moved Permanently</h1></center>\r\
\n<hr><center>nginx/1.8.0</center>\r\n</body>\r\n</html>\r\n"}
headers:
Connection: [keep-alive]
Content-Length: ['184']
Content-Type: [text/html]
Date: ['Wed, 23 Mar 2016 09:34:08 GMT']
Location: ['https://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1']
Server: [nginx/1.8.0]
status: {code: 301, message: Moved Permanently}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: https://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1
response:
body:
string: !!binary |
H4sIAAAAAAAAA81VWW/bRhB+z6/YKC9OIXJJHT6C8QZKpNYCHDmFhbZJUARrcU0uSi0JHpIN5Md3
hjecZNu+FHmRZma/2Tn2myG8ftjH7KCyXCfmcuS73ogps0sCbcLLUVncO+cjlhfSBDJOjLocPap8
9Fo8g+fLm7fbD+9XLFN5GRc5+/SMMXi+ul69W222nfXkxfu3y8V28SWVoTaywChf8vKu0EWsXv4k
0Gfo1YPYya7MMmWKMdslJf01cV7WPkOvBsnaWN+C0B1PAX22bUbsRAdjViU3Zo9KZmO2Tw5ardFa
ZjH+pHEiA5VVhkbeyL2i9GIlcxRiacJShpVPK9eQQhOwONwiMDEkrVKdJ0FtTNVOSwyxC/Ixu0+y
vcSi71NUMmybCVGJZYhqkBwNZZF/o04dPC1y2KjUflzVbfOnjtjOm17ZINhF63HX33+DorbacM2b
2CDtC62tjWtR/xSwxdkiEgus5w0/7JiGOXbQbc0pGwjZZjuueWhFpNYLau5aLyBW2wBGHT9X1P8a
NCQ3Qb5G9DPejY0NpB7krriSeaSGKf0pGDTrR0C/owQ0m0f4wFsRqnVVWWjniM5xAry7gw8vaZeP
AB0If3Yx92eIRRlwXsXHYvE7cJKgmk+xjRR7o0P2BvcMQyXJHoHXR0ADKiaedwa8EqGZSJF/8H4B
3mqAUyiiokhfcX48Ht00CYzEVaRdowre5pPzqHSKSDl3OnTuMBopGM2hAM6tN1/557xOj+6DfjeK
mefPT4EPLN0pjZD465DFqj+vbNioaoNSgS4W6FKBbl2gW0dzr5bb35zrm2uq+3uYs4mXVkD3YXI6
c5Zr/B7drm821P46ALRjusZ+z4EP1O6oSumqNKHMtDQ9pk619RBR2R/RA2Ft/nTqTfDn9IKeBQ3Q
bnyBsToZrc0MC/+c7K1G8HpuhVfhGwVoVLmAeiTFhi+ANzLgZ6IxoAD1zAnPRf9GhmqARIQuNG4C
uqHCC9EiIh1GnwN1r42mb3SNE8AHuG6AxPSCUu51GI4NpthRSEBLpo7c3gUSg9hMlL7Z/NDkrtN7
Sm4c9u8Se2ES87hPyrzH/Cdyu38c9NL5+dd3/ydb/elsfkbc/LHYSmTv6EdKT7jpHB+hVy3863bu
37blw93pCgAA
headers:
Connection: [keep-alive]
Content-Encoding: [gzip]
Content-Type: [text/xml;charset=utf-8]
Date: ['Wed, 23 Mar 2016 09:34:08 GMT']
Server: [nginx/1.8.0]
Vary: [Accept-Encoding]
status: {code: 200, message: OK}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://podnapisi.net/subtitles/ZtAW/download?container=zip
response:
body: {string: "<html>\r\n<head><title>302 Found</title></head>\r\n<body bgcolor=\"\
white\">\r\n<center><h1>302 Found</h1></center>\r\n<hr><center>nginx/1.8.0</center>\r\
\n</body>\r\n</html>\r\n"}
headers:
Connection: [keep-alive]
Content-Length: ['160']
Content-Type: [text/html]
Date: ['Wed, 23 Mar 2016 09:34:08 GMT']
Location: ['http://www.podnapisi.net/subtitles/ZtAW/download?container=zip']
Server: [nginx/1.8.0]
status: {code: 302, message: Moved Temporarily}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://www.podnapisi.net/subtitles/ZtAW/download?container=zip
response:
body: {string: "<html>\r\n<head><title>301 Moved Permanently</title></head>\r\n\
<body bgcolor=\"white\">\r\n<center><h1>301 Moved Permanently</h1></center>\r\
\n<hr><center>nginx/1.8.0</center>\r\n</body>\r\n</html>\r\n"}
headers:
Connection: [keep-alive]
Content-Length: ['184']
Content-Type: [text/html]
Date: ['Wed, 23 Mar 2016 09:34:08 GMT']
Location: ['https://www.podnapisi.net/subtitles/ZtAW/download?container=zip']
Server: [nginx/1.8.0]
status: {code: 301, message: Moved Permanently}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: https://www.podnapisi.net/subtitles/ZtAW/download?container=zip
response:
body:
string: !!binary |
UEsDBBQAAAAIAPwKWUBl5bNDXSgAACNfAAA3AAAAVGhlLkJpZy5CYW5nLlRoZW9yeS5TMDVFMTgu
NzIwcC5IRFRWLlgyNjQtRElNRU5TSU9OLnNydH18S3PkRpLmnWb8D6iyNdtLsibxBmRjrSlNl1Qt
danbJG3vcQ3JBJNIvNIAkD3EP9RhTmt7md0LjjjUiUYeSjzs5+4RkRFszZjJSiSI8Ah4+OPzR4R/
ebHdfkX/BZttvvWurv7gqQfhJk+3lxcfmnEummLf117h7fpm2hWXF916u3QPXnEaHsflsA7Tu8uL
y4vAEIs228whFm/iGMR+HaZy7x2f75/rjddWdT949+uhnKe1YQqhoRBvktihkG3CHBS+f2y8+6J7
5/3aecd+t/PGuRzKqWw3lxdCsJomry2GYj/OG29fekc14h3PEJkZ8k0W2TP4200cYoaf56rYeD/f
ls2+797QmFiPwSvuqnwwaYsxH8umeXzjfRreeX/873/p+puherzt58uL4/OEtXlNeb8ciqmrwMPb
4li0hXAsMZRByGG/H2/yhFZTHLul6Vv+lF96MH23XI+VVz8Ot8u8Kzr+NqKVGlrJZuuuMt1EPmh9
Az4diGGHtXvgMZkZk25ihxvBdpPS9v/vjVceSuyW/W29Oz0zKdekMDJLHFLhZkukPlUQFOx6t68O
3n3ZgC/T8zCtXVV7VfNQjm31NVGipZqRuUsKcpSRHHVeNQpXb4upb72urMs9f5Lvm9GQIXd0uglp
tz4VhwIjWvebZLSR4SDb+Kk9OtzKZ7yfvbXTu/iKLVjPsRBCRpQxLnO2IwxlGR8ab7rb93tvKj2o
gHdbdZ3igJFSvIutswdHJLbYy/56XMax77wWe1oeHrxT1TRFV0xDwfvhG6nFkNxdQKIkXWR8441l
21beoe/20KInYS2t7VjQF3WVfJERVoxP3EWlm4DU+//Q0Jt+PyzjLIswQolXwsAZk29iEvA/lrf9
4WHjPXnXYwEbM3t3XV8Xz9ejUDAiGm03uWNUomiTBCQMzZN3Wpv9guWSiRp2a/NQnYoB4nl5sbsb
jks3P3rF9FDcVDBXXVHLBxmRjWJiiE0620TEoR9emuduV5K0vdaAU99N/QgFYD6x9YEMd0NpLJL3
Suc3bCONcGOO2OFi7G8yErC2gnLQttbFaaJPUhwtpuryooHeTGXVlhP+MJX/VsAWqL/37fQoZjQw
OhCHZDbtSSKxBb+ULDhtNTw13q4c5xV2noUvMCqAd5OtMxg6EbIms6Hdl/VzMz3PZH29m7URK0zb
6U1FA6tVX2GW4rj3vhz1IifhgtEOkAwsLvjkiHxiUjmTtSi6h6X1lql+afZQE0jH8wj+jpcXZ6Z4
whNiiWOA9T4HSpuYdhY4k8WbkD7o21fE2Do061TOVc1iGMSGRrJJc5sGOQVS5ytPRBlD39Bv4HCz
TFOx957qchLWJpoKBqXOSuAAtuTffizF1MuH76oZQvZ56uHThsem0Ow1DCHSwtHUkA43wdYhnWxC
0rR1hO8Y2orkuaHtH/CRN/2h2HeVJvwEqaqf57J5qem1sfTu4fKFBZmZIXVMIx5kZGnJonXrvI7q
/dy8n23S2H4/8OVj/wb1gZucl5t1WMfy8sKWdU+L+m0pghluNUGM9xOHYLDJSarfQ6DN6755Hbx1
mA3PwK9/pE/+5Z338zvvQ1P1YDOEep3nsv3q8uLtv+OPqwAUUrb7qlkOm7dMOjCk4WsdZpPbIPF9
SxJEZnkASCk6mOqiAZOvx7vT0vEflJm7HsvmnVANNVW4jNhZMNzANrMt9k3ZdE8QCjDuBlbhvhAT
ehr6G2MPWC5CI/yg4e4azDi7eDB9qOa7Ydl7p5J88y2kpBhhx5b2S712E+hXw2fwYIa7gQr6eZRc
9SNMIPQZUiT4IzQqAlufOkyBrc9o9/5fcyTJgyCW3aHc77XUsSNn602Gcs/iExpdwejcEZ8IXyJY
kFXltvBKGK8vA5thrI8MI0kPIAHJ8LwADMoSjZLEwSZyOAxbF6SM/h7vsVNkdauihZKMsGswYsA5
TdFWCkWGRhfInjqLi2OBhD+yGcCrSg0CWLtNYEltwDCZfPlH6Nw6PgHMlg1PWBGXCccKPppK0XL4
6Le3FYnTRNvyFp/W19Nd7T1N3hEIFCZwVs462ppZI+K9PWtGu3N58c/VH+J33npf7Deen73zoIEA
rD8s2FTvbiiaSYn8P/9T9Qcm6WuSPhyVb5ME5ONY4dt+2LOxKr7yAKMBKB4uL0b54R1EthoA1up/
ObRF1by77tvLiz/tn+bPUwmQ/JU3jzA11Y7nCvRc0Ow4deaCxpEs/fJ3MKgcvvL+RU3wvzB/cV3u
+r42s3tX3p+ruuSNiEJNE4DIdzYiBFIk9frTvoQxhB4UHuADuAlVHYsjvogtShQZCvEmjRwKKWHu
y4u/lgPck2xBrN8GvMic+aJkkwfsRyegngoAX/neKDFjQNDhMeCCAD9YxebxoEEF/o8J27LdqVlT
TSHevqIAmc8c2EdWqCtOPRQc8ocYTIhCF9lcCb3M0AtIsG16wJW0oh+ex/m5e4bfYmWG8b4vaooS
n7uJwh9i57C2S9P2QMyyF0Yp4pRYb1PNNymt8gMQMQDQcYHEeHW1MYCoZR2/m4p5AfbB/+Efl7Go
dEwVK9EPoY2OHQrJd4cx2/2/F8Pe+59900OKZm88lddV0VTjhFVDk0ooWAk3AGvnfd/fdgSz/+8A
v0igD+rHljX2zTxM1p4Hweo2o3nuxqnvaATgALCatwCjt4xLC9jOoe8KfAA+r15PcJxDyYQDQ5jo
5DZhYg54d7tON0/eUB4OQIrZFdCYfHqoR/ru5uNBIHD7T4eyE4Axe3ApW+9xKDB8o2Fn4f34/uf3
V0CSnnBbCEeGcLBJXcLRJs4Zx+v1dA99c43Q6LNaVGzGJhvf4ROAge/LWLWWBaDN65v7YuzFZMeJ
GZ05uCYkqxPRbv5YdXCicKoNPGLpvf3LWxmZ6pGAC5E7EqLMNoTCL0pEyFiKZWk4q2KcmeEhmWl7
eEyxDMfGxWmB3P82QG0eR4/ccF9vGGpNgA17ElpgazbnQ3V4GNeDZBRyQzxzAETIvp9s6a8jLWwi
gEZAlvzOe5j+W0wBC8VLTIykhwHhWJtKrHhDQXZ5CxxVawDZLHOxf/SmqqsQNQPhwHzAwR3UVidG
rGHT8sAhmm+i9Mw2Y0cGfCokGlExTMdENgkw/XpcW7Imi8p0GKGmwC+1ySKwSwX6wmG7cb0MNVKN
NzN3aEKxNZndrgMAonBam6HjI3n+br/W3j3FEmVjMkTwRFhX1/RkmTBidw/UV1fkhGfsUFntWcET
I/QwxmlizwvbygEv9AheiNDC/TL0Iys6gB+76vr59NIcXyhtNhIbIB3Hu64uBK8nRiuElk08kPwA
IUjxDhjmDXe3CwZjInj9bh6XevfYCCmjIq9QTUhghHcM8ki4aJ7g0G4fpxkgeVB84uVypHoEkgA6
Yx/AC/VODXB4B0uFMEMhn8RoVewGzSGHcg5IbVbQx57elxLLJ0al8KqNmiIy1bGgJsw+FAeKqWH4
xVLRghAQNk8UKyDCx0+UPIJX6REM3lRflIgppYo4rEsd8skmyjlAaUkZOnJ1nMYxoTNISxZta4gk
TvQbkTVOQg4yvr8ruiv6hxJg+7459IgTyTXfl0Pz4H2smmbcKVic+oZgRp7NIggcxaj+h+IEG0He
GEoDbw6NXRjvFfi8zwMFSCRFN+Vx9zQ879VOpIEmDDp+5BAOJST9bzCYVzuMXI7MyW69LQjvtwQW
2TM36/0T9GSth1IlkKb7gqmHhnr4ig8U/hEz++ahIHg9QfRJWhDtQmk57mirbpJs5R+LbpaMWBoZ
ijD7mU0xUM7pu4d+GKGGw34dv4IF7R6a8oBg9+4wAFRCJgXe/47nLNvhqeFZYj0LEc1jexZA9eQs
nwbqVNCB21U0fyjZsqaJoRJT8GxTUdjtxwL2uR8BUmCkycZTOsWy80InNXRSSp3YdDLKALATYVfB
GpJm+n1SZWdPgbaywEFvzUApKLJhu2JYJgnw6afuydsXDQK0cgS0FuYb3YgTJ+0UEQLjyMdSPV5L
ZjQBb0TW2mOKZNgBqrQJeIAocKQs8TrCmJAMk0O+IftCLNk/iSXPlC4whSh3SEaS3482iffQX8P2
r0D3/3RTtvhxARo4lcM15X3GcleO4C8jjScNg2XFgSEfU0Bik4fIsX0pWMvKbk/pjXZFsFuQ1Ffi
d2AbhFKoKQFI2U4wJgAUSDab4ULh/XxHS7ubRqqT8K5nkRkdOOm3mJMzhHj+OjzuloZ8JdSeEoZK
qbPYDA03oe8MTUgiYMPwBZCBB6yfAmn8Bv7uyAFTEC2WgrICAFJk9sGk2vLDWWImcAUBD3Kd3YPy
/jz38IwFkfiub/sBRgFEbvoBOP4R9ABFGmFVqglS4j+zCUIDQy4hzZxPq4fPczXS0mdIpfrezIwO
yFXZoyMdJFEikAh0z/AkM8wWnAF5lgrecdJJg0JitCw3BGPK19sEOW2tsj7tP1BteXy+NeMTB+ni
QS4+6rwgBAl7QKgSluebaqjLDmj/uu4neNZhX3BWDRSN0AsBi2IYyorAZAKKwxNCmZVMyjpw2pgy
8j18MIKbgQ33278th37/9VtlI8dSBC43gh9ygcSeIqFYXYAgRJwRkuJ9boQcL+XuwnJC6pQNKQdI
A8ZCRv+NVaQj53lLjvcLMf6o4sTcyDxAWuBoDMJfTjj8hfxFx8mVlrJ7fdtUNUMnaOCePhB8aPpd
f+Da4PPMicoeoLpHfKwXbRSE8uexPQ+AVMT4Y35C7N7BfWyw7KdhooDupdEEjALE4avtgGn0M4US
Dg/YWEqOTmTleStL6OtALs4T6RspA14dNF2lBwllmW0wnlAplUN3qSyy2IrByjMzxq3/JRxTqizN
FQDF0j3MPXGa88qYmgpe7Tudm8lzQyhxYp2ELF+WaH9FyaUjmeSyGZ849t8jKpBCy3ZraOROwJMQ
wEjUYj72DU1Pkn+EyugF+Ftfj6aXM2d0JCAdo3+oTmz4wMRJchdfmoepgAEeIdjTuzO5wJCLKd9j
kwNnIgkaj49vPNJm2OG+2ZPtw8apGmJoCCQOKE4Iv7AFx3o+HOCrvBdsNlzCm/PskRmcOqF9QmaK
wQ8GfwOzAdFV0G0oAfdhh0W6x5nUhu1U2bEzJJGiyAP/rR7WY5xaddicJ471xGQ7nT2A8cjUxILj
QbDq9ncNNIloks50kE7v5rmBc6NUc3F63CN62LCfgJf+chzYoNP/rTkTPSdVOB1OIaxkgIo5O7YB
S/sZ0Q7V/oGZB7gE/HSk9CsFkMvEktXu4M3O1I1awNjZ6c+E0nAcRH0EkWlp1L4ZlcCfQ/f9WLI3
WA1lMaB351mM/FPg63xDtBWchVG/chRU0p7vOLcDdLm/G+6X3U4FQ5wDQeRFGQXvMBT31bRcV48U
4NGn0pZ5VItpAEIO9gJ8ozxR4HjBxNQSsQCGxSQHBwSdBJ8PvDfYtPV0wrQPFdC3ghVgNqUmSria
sao/TwWbwPOERt/irRNPJ4QUfcWnH1dKjhsZvK8YpPDiz5SMqsH+pc7SEablgdZ7qurd7Tuu0VId
ACboHqFUdVYbXfNOCd75lu9MKb7jvYbSwqn1WEWt7Kavi938ki3zKRszpalcF1s7MX33JXdV3PdN
B6vGdWMOY4saBrrQGQD52uPz8fjcmQ20vjs282akWta8AH2Zsnc/0k5QScBhfqKHEjyMnaEq/Y+h
QPbKXUqkJJGwZIeofFfSn07L4BXN+AhH87BMZ3uqK+dCMXOmSKV6JrnmjUAZ0R5dLeeXfGdhAdeU
eGGfykNDoQEZ4Gs4yqG6uTtwxFlIdfCWGHuC+xukgC05qLPJ0JVzoeqwLoCWKzOlkw6UYqIITbbl
8mL60nj1c9c9lE37G2Y7FXVPUPjMX10kT78KfUebUoZMJEhOMV4+Xle9+SU70k7ZKqi+EcnRYVkm
gaUaPwIz2nUY6VeRrxNULHOT6pdQnQYI39rl+EVC0yNnnKRNCvDjy+GBRZGsrwitmsxoCsxF4Cw1
iihzqzJ3ks9CYH0srigRQqDj9Ng8ajTvB0Z5MM5VOSoyRNKvAi/lDcVxKmQLqLj5qbiuKNw7LcCb
hJLI8kkd0g+MZsB22TY4pR6FlNDEx+oAqwLDv+Hy2zi3a1MCca0NtSh0XamCYylgQ/jFWEglxg+M
/sSxEyClHJ6Gpg6nbURgtAEmyU7FZWRqGJv8x9Gbiu6uaaHmXDFo76ZiAibYCAWlGXqATSGSpiLe
KfD4sV0or8OpEfZmZc0ZDSgsNUuIERQ3Xz8PL83zNL3UHiwN4Kxabm4mQzyb25MhIuRMlHwfMY8C
UK6HYqeVseI8/M0Kt/jUiL1gT39U3T4QtL5pHmH3pqk/LPjrQLZtpxp/dJ08Y+zkMAtWgbXnyvu5
PHyeOC/8Nf3KDlVG+2Z06pTtM1J2P7US7lISPb4IM6iK9HwquweqTeKrxrljoNetteinrppnBG7s
ECGj7AhbjX9VppzyOs+niZOnp2q4ayj1SdC5H/tOJYK8/B22h3oEsCvsR2sCkvt+pmie+EUvqqlD
M3VO9XlramAcnSnn3C17GXIpHPByggioSfr4FIciTQyWwuUvVb6J/dTnqMMsXxfG+c924TQjcMLF
zL8tA20ulXy5lYtyHZPazUSPxstJ5IwODTz7hvSMQDTC/rND1vVufjVxPhtBmU+K/FN/i6Csp1i2
adR6japErnZmnAhXbpwkkCSYO2UocSK1d/p1pxfTj4qi0Qcq91kcy5XPP9fdoRCIqqB8p5I2n8K/
67Fe2raQZLevK9s5xUx2D03OrTn0ZywD+1dftdAPkgtsKJzNwG1K1aGYVY+drwn5Lu7JORMixTKQ
IodBFl+NCswot4E0pzKTCvN00Ez7WdTw8KS1FVf/oBCQSmGLrkjnlOvYOisIYlGID7N4HB2ywcqO
Hqw/DO6i+h11VTp/navMOYXBOUMYxKq74f3inqSmoKyFGh+b8anTOJqTskRqu8n3DEvzAABMgcUe
1gZmHy5NpFQXrnmMjQhydsa0iA/DULJ4cAqeFJjbCz5Pj1M/wGbjDxU2TGuNLmTn5LxtF4kHmeQ0
gOdLIjqWZ5rX45fr8aY8/ubdVIc7aK4QFZKZJkk5CmfvyA2zs5QwoO/2EzfslTPcPBU0YU2I76ve
utyQcrFGzkoTCtOU1L0SodhIcOQGKzn5bGY4dTmvQ0m5Li7N7dVII7JR4hRfco4ySGQ/UcBASsR1
XmhBD+mDBPJSVuNQYyPGlEZxhCZWZVXsF7wdwC+1TxPk2ElcApqKhhFfRBxbZzVw6wwTqEu1siqF
xTCsu50n6RVFxYgvBmUulURhPSpUKg4YYaVgxX07p+jV9OdShrTdSC8Q/yJ1k0/rAYpIhVWJZhVZ
EV9/S0bFCvjpQSzhm2o8V7hGeid1f44ikhoiTucxPUglYf7d0/yMOJr8p2rSzcyY1La1eEC5Yxrz
S0kwkvnez30jcb73Tb/bPXjfVuM1lfLORQ22v0t3VFCP4h58PBjy0qiOMF8Vn2UKq7vY123olxef
OXE/sSIUHZW4ydfX1GnGQRWl11RmH/GCkFXVaP91M7vPHenceQIDAruunNwO0GWQhERdzc91DZoE
kUmLuaZGNSVEmZxy5pTTsNbsj++o9Kl6kn0zKaBqZk8aqHTgr/uSgw85B3B4kM1S9Wh5z0p+0oOA
/OwZEeiQ4agzZaogLa+mjrQEykF+xNoRQh0L9qv0yRysmoMA1Puk24KpDAETpmRJBRcVVXkfCoBn
NWdk5szt4iYeIEYK/uGoAEUctPbVRiKq6CxjIkdCKTtDW/Tnsu+KgTIP+OhiWOY9oVHoX6XWYTQl
DF8JbKjaIrQoYoMRYCgIkBjlAAiKnY2ixm5K60oybuVeXopChxKQmxwoB/WEzwnZcYM4gpVhvmvh
g4p51sggMbokFO0pQgnoVZKHoDxtj/rx8oIA1zpJvp2ilbKZyuG+nDXkS4zKgFLuqAws8TbXPnKE
mJQwm/Xzfh2Z65TOuR77odiV1C0J4e1URJIafRESNs1cOEngdx6LtrhfGulxIpDlIeps+xmKcDUt
O2rnmExO30+NQiBUCx25jkPp0v3E7qBq7peODLsOyzmNAoU+wWaviMgRVxD0UGSNulAd0iWrsPtP
AOP1G+/PJZVTOLdw+9v0xGVZMiWFd7gDA+5GTvd6ZuUKQKRGpxDhWeclfO7azrPfaTuhajafAZJW
CYrLKpPH4z58MvBK/lT1mciFNminB5G07P64zrMkk/hIBUVkZJyURjzrhcaGUESpYJtQIom375eB
QB5VH4qa+yFl09lzcBJWsuCw54QF9c4lhnBioxN6oA6uqJzA8Y5PT1FaRlkNfURD1ZtlSORyMReX
/D8oXb5Rkl9X6vjCa3LqWzNNjtq+HKb5kcSxEPtVkDXzXCpW46yWk5vxkY0y6EEsGvlL0VA/9Ez9
OsDG3HgIKwlDMGiAqwrRMsg21dxZzqWFP+3LY6n2XsqClxdNMS87PqFU7qXRR3yKIuoborljj6Q7
3Vfov9ib81W+Ki/LG1YQ6XP/ORfnP5WHgeCaHEoqWexr6qg1sFZVlmlMZKNteZCdFUnejqy3c3eV
Stg+SY8YVSGap3F+HlTPkD5KlBl5DV6LFUXd4e/XBv3MSCOdmgqdYZn0jX9DhwkYCqkOMzXSyGCQ
2X2vPve1c3KUR/JAFrfbUm+KkTf4J99hDsJ0LmO9l6hBte8QZ4fqsLSKXUbewsjGxz43u2/VDukT
a35uBAt/9d3XMwU96UTCgc8+1hCjShnkpWUw3JDJUbSMPMFDuPyKVC/eNyWDAYJSsIDkp0uyu7qM
fTg87nUQpM6Q5UbiotgudtIDPj3EGc1C4VuuafBwIdat9bnBHmo29wp5QeMexAflRhrd00h4QG1p
NAFQn0BeBUAnajShXN9OWi5J5BQLlLAG3Dplbbz0nGdcev97xRlrg3boHKCKblRpV163EvK+dJyr
rPKHccLoL83aPLEV7dhLUYccudo9d9ueM8mq2kskQupOs2lG0u7ASX7yGY91YQ1MzcDIUZuArVB0
HghHZIZbNQJV26X3U/u4HB4EoSmnvhdwQxwu6p4dGIG3ZjneNRKBmko4JPFl2IPf/Y6Cb6pFU6e9
lTZXVWCaIbaTwT53zXOB9xfDuEJOJKmqr3rFYRDUl1Mz3A8iuT4IcHuq4BnndVTjfTPelaCAdDhX
DH5/a9VpqLGYjhrQ4aJqLic6f0iwr2nxs/6YQFWAiU5gR5f0IBFbIDUG9v3ERSpN/UyNKdNtz5FD
caIjER2Lqm5KK7yP5The/TAghhkIhfSDLTCBqhvTLJnd/4EHkjQAvl2mgRrcyQBgN6a+Vqww8o83
Q0dkIp8CfV3IsmYzIo83XO5FqfSGcJUaOvtwV2ubI4JBKVfvRJiKjo/MM2D6/XK0BSLYGukHonQ3
FzAu9E1ZVHaXcgWUaBoRMt9Qzapd6ABlWX8Z2XZQ2+Aqp5qmYrqz9sooiwT+9jypeEa1V08jdfVR
33G3Yusna61KX8JXxUOf+/i5dPWznFwWD1+vYvQDVfuV96zMPj0IpDcdk/+RzzuKayQenTU1ULVb
eT9JHQKhHBQDAdacyd48VYJVr2XOuNScsXkPVNL29T1VWzA3HbWYuNUQUru0BTaztkgGhmTuhLPc
Zx8pkt+Wze2XWRooEGLtHluOlG+AgVo6QgBG3y+D/P2WDmLCoByf7+Gim9XiuCrX+tyYnzmMg4WK
MsO4gn0IScALn/H0duXhbvc433NPkjQ3V4jD9pTyJc94lgxV3CWSkZ379qVnXc0xLA1FXK3pH6g8
MLuUSvPlBR11QGi4Dqq5YuqpZM+BIexGV51ni/VsoVOF97kh3lc7yYcgvWXoLbVXFV150cqy04OM
hEK1L3m7ZaJLC2DxIX3fvIxzXer8gAasgard+tzrvnUYSx3s5yqslLHPh7cPj/b2GIWAb7aS9770
eau6uKyKLmJgP0h1EEe2jXJgzCsiCbGIiXz38Nw9PA+/sfORYvV6W0r8V7SmnhCouqwfcRHNohaR
6gTqXPH4eTBHiX0zIHDyatyjvU2s2v5osuamokBqci7YB6pCK2Othmd6kEqnIbUYQduk3F7JyT/H
IqrCKw3JnfCYu6iDTOvsqRqhoExIJdwYCNbDahOLNDGAi8j5Ol/BTQV4buW+AT7uTw2yh+qgUfrG
OzyUA/XezlK/C1T5Vaj4Dpcp3uE7CCY6oEPtq3LYXGr0jT7wrOqrNCC3zwH53BWt2U7ntrDjw3JY
JGdES9Sxy03ZWB+aanrU/uR8aKDqSNQiUkgs0HWrVNG4+4GvDSmgzGejoGqxZrRNLpcWPOpyqvl8
CkvCE4mCd86jXV6oIzfk85emUbiJDnZgv62F52am3PENEaETRvi/0ilnXQtlR0jWTsQ3NPJOfQjO
dyPAsMT3PGNoJJ5OGWbOkMT0nVG97433w0uzP/IunjtAzsoWGnGPnPO0eEAtP/65XOqdM24wIfOs
ri0JQiPscMtbR2HiTM6E/7AOtDUam/2nlJSkxwTtrYtC6EGuztTRFSgbz+QP5RoAKPFcyc0xINgq
tsaaGp2ASGxqlDomvfnm3DfAajMV5rAwdz5SvEp9UkJWpF5VSn1ubfa3DtlMFmlyRxCermgqLzT3
lwSqWCpvR86qApVh/ahHXTFUov4xvoNGuKWSMIGqn/rc05w7zIITjLnMJChOTZyb9yM6YWK/n0rP
t+IqA86Kkj+fD6RgMq86JhWoyqjPjca+QweBN4OHv/ZUmlfHfmR2VQaVlxJntWEouEvPzvbhv1xB
YGhF9nUfPjcmcwngGxpARaVHNX2ohxBodjaNkrZyq8B6eOPJvx/havdc0CKgr0gY4QTKTZyNi30V
9VM39c1QdNdfqzFGBGM3lxOTo2Q1VWfKSjmHuvdUBjyIjJjFgZOJj/kQeX4WMzJORkjVvTBBZMQM
b8fu8MS+KIf6hdZGBqvrIyIjWdSq7OwVcDbXvn+qrm9pr74tH7q24H45tUeqChpERtwImztiEuci
599xFo876WrVsau3ODZCFrt1iITgNye1Xy/h9vMALHFTQW7urJxyoKqZMtJqDfW5hTq25P539VWV
MNXbuTM8Vie1Zg4thz1MhvQWvF4bZ2CaqoHrKjoNoQhjUr/gND5yngd+1XBPFT1pktTJbiV8p4cR
HK7ZUWYUEE+X4mplIFTJU4bkoUMjlDLuP6yTULFUo6ZlODwoQrEhlDi5aGlyfnVjRP2y5+sXrLRV
oAufrzud6UEuHu4nOdUrx9y6fi9JLMTV2t7pumei+iYsGsFWFEKyvFKTnvjnrtQClenRVLBzmEHV
tZSlgNpLhImqNRFu+vSZToZLJRECfsXN1i9g98hNH5wm5P4synEP3ToU0hWk2y8CXQdNCIZEmTOz
6tllZIpY6sgO8g2FoP2uwA6MlYeovJxbxKPCSV3+5MGxwwWyo9w/yG3FiKzruaQOuRWurFnl2DD3
LhSnE3gLrLhSR7oE4kBvd9MiKeggMSoDA5u6k6SyXb+3wq/feJ8ohXdfXV+X5p6fxCgQxrr7RnEL
n4SgfPo/3AhBOrE2L526GicxKkEm3JHCKJAc2+8tSmWag8RoQxQ5aZOEsp7SId7fQ0FXs3Aj9lH8
SmgBlTjP9f6WhhQeMZoagQF0r6vru1FNaaSeimSOzOn7kainSuwmtyGtp+vxqVOt9iT8pRWpJUYB
qAfBYkDKYZGK937uOQrQ0Io2/PfIngGgrmdyg7Jd9+C2Yy46cifvqZzWUW1Frof4TmMuPQgoFJV6
gRzXa6g1opqUb9HlSH7TRg8pGSUuHX0vl9kxXC4RciwTH/Vr+KySPomp7KSuQ/LozCWXqBseIAn7
l+YcGXluYHR5odPlBd3/QIloKdDW/b6S4+wq4NbVSd0bbU+WSRLyP/hmCoqVGlVcCXTJMWW7ldqj
qNMx2up75l6Pi/S4YGvfBeVz93OoEk5yxwQ50NvfGp28pXzn/rzDupaY8sV2iUMpF9v3YynJ/KM0
2jac/9tQKwXBILoARsWnOiun1phoynT7m0OZ6u++OMf1SBVKufHptHZgeH/qD5To2ZUasx+Xe3XE
K9DVxZRzow7DIl/dxUZop8EucfvILV3RoppH2upmbcrLC2gkHUeZHsqrE/1L5tUbFuUNUiPzMAZW
jyg9iFUzm8oC3jSP06LkPjVyT3etxM6wVNWcdERRV20Jr6DkNDNiH2X26Refe505Pa5jIhED5T4p
nwwIrKExs6qTzjzVBRxkRgXi6NWiNNL7IO1rLZ8YeqkR5UrDicFImZHs2Lk8w+dO6FCa8zZmMPcK
Dosaa+Q7drMA3AMt9sNtYQp0vZHfCNwhAXUyW31Q1kHk9QDnu9s98an6JzC3nPCLujUsNiQD+9Ic
ehALYPzOJEB0L4K6AWIcVQ5TXwoV6LIkD7YjjIzwGPsshdvJGpHSSse/4khqRvMZMHt0Jim1D3Qx
1UrlNGnTUgMzM9B17pmxq3S4iHEjgsIbuRuQjhXxUSLuXdG3nO29/+yWMzVXrufyY/uQtM8N2Bzl
6ZYm4jzdvvFI99ZUI+Sa7pug6AjWANZhsvpdA13tZDJ2pJVR3Jn6HGndk8ZS5vWP1diVD9RPvSsh
6/vqEYLyNImE9/r03lzdF9Pjfq9acAJdBc348s3QmSMV9cWaCfaU6s5OUHn71zu6lgD+uXh7BVKT
vhp2x/eV8t1mQj0w1FO79dfntm5uXFOnqTmp0lL/JB/95MPdyzQtSiZ12TOjmNd35EAXrchNchvo
0gB/9c1xGVTFLDc6EkaOo8w4yczXwcrxWFEtVdwOcqMI1NDqrD58DdY/H9aBNlgdAGnNPVG9si25
0QOMjZzNBHbjWusHuWLNs29Ye6VNudEHuqEts6lEgXSxvm8fZH7d2aGYYBQCUXrgaDXZ6TPa1ZdE
PA2zurKSDh5xPY4LWASIPs+UJuRWXjquPpTqG40ixC4wzMgYcu3g3JRZdkcNSMOtkXQYPtv55Xy7
JP3ZvbMKazkfSa0LBBirvvs31AVOHmtd1EIPIjGJwBbUggwGgciRu8DoeKQEl92T6UmauJsWu0hm
cqGjQfpUc6irnflrA5NzWOmLrWbzXtIFd1q2r0Z4taFS9yOGmggdYPMdIr4Usv51hEG4Xwf1bZEZ
EDjOLzc3G3OKUQXDVOfhKwW4EDhJK3mrPiDWpOjcfWaTChIRyE8Iv6BGE9+zo0YlZlTqNGzkpIix
uo2AuvX0WQwOhtyrWOGJ5ZY+yQwSENoD8XePHO5MaqZUz0R9i86nhupSYdWsvErSkc1pyQFmSU2Z
imOZIZM4PpW70gPVNipX6cIfcONuqfMALV/XqdaTa0KATnaykjvK+bJsPrGthGbUd1puzbDklTTG
WxHtD3I4fVwpVm4e/t73gMxaznwjzXHgZPu4iTtV56oFu1EXH7XNcjuWGm7ENI5eaVac6FBvPXkt
3R+08dgVC+N8I5t4MXBHpgZst+VB9d6rUUZAqcvQXW4uMPbD+SYI8nWcFIAVeQCu5cPP3H1Ix6X5
GAZVlWbsrfocI7SgZlVAgy0ns6y2b/W+iKv82bpJNpA2cQaLpgkXi7hrmmU8gB9Ur66oe5UAu84l
hapOSKMTOwAPuBucN/MnualEvZ/p9/3Ivt+EHiSi38YgyrXqLyrXEao6IL2Z2ppOD3JhP/Wy9HSq
WrYci/Q+FnsqV5EKtNQGPD017LGnl3pQ/ZChqgkG0nAd2ZQDX85paGOrBvhmQGCrUMD3fQsT58/3
fCJfBgR6AN2G7cygb8Pm/hIskANGuZQY88mnq5JfwJ3H7ibrO6j/y3sj/z9QSwECAAAUAAAACAD8
CllAZeWzQ10oAAAjXwAANwAAAAAAAAAAACAAAAAAAAAAVGhlLkJpZy5CYW5nLlRoZW9yeS5TMDVF
MTguNzIwcC5IRFRWLlgyNjQtRElNRU5TSU9OLnNydFBLBQYAAAAAAQABAGUAAACyKAAAAAA=
headers:
Accept-Ranges: [bytes]
Connection: [keep-alive]
Content-Disposition: [attachment; filename="e638ea178f406cb584f48051501e2cb4db4fce1d.zip"]
Content-Length: ['10541']
Content-Type: [application/octet-stream]
Date: ['Wed, 23 Mar 2016 09:34:08 GMT']
ETag: ['"4f497ffb-292d"']
Last-Modified: ['Sun, 26 Feb 2012 00:42:35 GMT']
Server: [nginx/1.8.0]
status: {code: 200, message: OK}
version: 1
+95 -44
View File
@@ -1,9 +1,11 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# subliminal documentation build configuration file, created by
# sphinx-quickstart on Wed Oct 23 23:24:28 2013.
# sphinx-quickstart on Sat Jul 11 00:40:28 2015.
#
# This file is execfile()d with the current directory set to its containing dir.
# 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.
@@ -11,28 +13,41 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
import sys
import os
import shlex
import sphinx_rtd_theme
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('..'))
sys.path.append(os.path.abspath('_themes'))
import subliminal
# -- General configuration -----------------------------------------------------
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.programoutput']
# If true, Sphinx will warn about all references where the target cannot be found.
nitpicky = True
# 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',
'sphinx.ext.intersphinx',
'sphinx.ext.viewcode',
'sphinxcontrib.programoutput',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
@@ -44,19 +59,23 @@ master_doc = 'index'
# General information about the project.
project = subliminal.__title__
copyright = ' '.join(subliminal.__copyright__.split()[1:])
author = subliminal.__copyright__.split(', ')[1]
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = subliminal.__version__
version = subliminal.__version__.split('-')[0]
# The full version, including alpha/beta/rc tags.
release = subliminal.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@@ -68,7 +87,8 @@ release = subliminal.__version__
# 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.
# 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.
@@ -91,29 +111,29 @@ pygments_style = 'sphinx'
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ---------------------------------------------------
# -- 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'
html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {'github_user': 'Diaoul',
'github_repo': 'subliminal',
'github_branch': 'master',
'fork_me': 1,
'flattr': 0,
'gittip': 'Diaoul',
'pypi_downloads': 1,
'pypi_version': 0,
'travis': 0,
'coveralls': 0}
#html_theme_options = {}
# html_theme_options = {
# 'github_user': 'Diaoul',
# 'github_repo': project,
# 'travis_button': True,
# 'gratipay_user': 'Diaoul'
# }
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = ['_themes']
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
@@ -136,6 +156,11 @@ html_theme_path = ['_themes']
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# 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'
@@ -145,12 +170,7 @@ html_static_path = ['_static']
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
html_sidebars = {
'index': ['sidebar-intro.html', 'sidebar-star.html', 'sidebar-pypi.html', 'sidebar-donate.html',
'sourcelink.html', 'searchbox.html'],
'**': ['sidebar-intro.html', 'sidebar-star.html', 'sidebar-pypi.html', 'sidebar-donate.html',
'localtoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html']
}
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
@@ -182,11 +202,24 @@ html_sidebars = {
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr'
#html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# Now only 'ja' uses this config value
#html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'subliminaldoc'
htmlhelp_basename = project + 'doc'
# -- Options for LaTeX output --------------------------------------------------
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
@@ -197,13 +230,17 @@ latex_elements = {
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'subliminal.tex', u'subliminal Documentation',
u'Antoine Bertin', 'manual'),
(master_doc, project + '.tex', project + ' Documentation',
author, 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -227,28 +264,28 @@ latex_documents = [
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# -- 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)
(master_doc, project, project + ' Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# -- 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'),
(master_doc, project, project + ' Documentation',
author, project, 'Subtitles, faster than your thoughts',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
@@ -264,6 +301,20 @@ texinfo_documents = [
#texinfo_no_detailmenu = False
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
'python': ('http://docs.python.org/3.5', None),
'guessit': ('http://guessit.readthedocs.org/en/latest', None),
'babelfish': ('http://babelfish.readthedocs.org/en/latest', None),
'dogpilecache': ('http://dogpilecache.readthedocs.org/en/latest', None),
'dogpilecore': ('http://dogpilecore.readthedocs.org/en/latest', None),
'stevedore': ('http://docs.openstack.org/developer/stevedore', None),
'click': ('http://click.pocoo.org/4', None),
'rarfile': ('http://rarfile.readthedocs.org/en/latest/', None)
}
# -- Options for autodoc -------------------------------------------------------
autodoc_member_order = 'bysource'
autodoc_default_flags = ['members']
+40
View File
@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
import os
import sys
import pytest
try:
from unittest.mock import Mock
except ImportError:
from mock import Mock
from vcr import VCR
from subliminal.cache import region
vcr = VCR(path_transformer=lambda path: path + '.yaml',
record_mode=os.environ.get('VCR_RECORD_MODE', 'once'),
match_on=['method', 'scheme', 'host', 'port', 'path', 'query', 'body'],
cassette_library_dir=os.path.realpath(os.path.join('docs', 'cassettes')))
@pytest.fixture(autouse=True, scope='session')
def configure_region():
region.configure('dogpile.cache.null')
region.configure = Mock()
@pytest.fixture(autouse=True)
def chdir(tmpdir, monkeypatch):
monkeypatch.chdir(str(tmpdir))
@pytest.yield_fixture(autouse=True)
def use_cassette(request):
with vcr.use_cassette('test_' + request.fspath.purebasename):
yield
@pytest.fixture(autouse=True)
def skip_python_2():
if sys.version_info < (3, 0):
return pytest.skip('Requires python 3')
+29 -79
View File
@@ -1,76 +1,12 @@
.. subliminal documentation master file, created by
sphinx-quickstart on Wed Oct 23 23:24:28 2013.
sphinx-quickstart on Sat Jul 11 00:40:28 2015.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Subliminal
==========
Release v\ |version|
Subliminal is a python library to search and download subtitles.
It comes with an easy to use :abbr:`CLI (command-line interface)` suitable for direct use or cron jobs.
Providers
---------
Subliminal uses multiple providers to give users a vast choice and have a better chance to find the
best matching subtitles. Providers are extensible through a dedicated entry point.
* Addic7ed
* BierDopje
* OpenSubtitles
* TheSubDB
* TvSubtitles
Usage
-----
CLI
^^^
Download english subtitles::
$ subliminal -l en -- The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
1 subtitle downloaded
See :mod:`subliminal.cli`
Library
^^^^^^^
Download best subtitles in French and English for videos less than one week old in a video folder,
skipping videos that already have subtitles whether they are embedded or not::
from babelfish import Language
from datetime import timedelta
import subliminal
# configure the cache
subliminal.cache_region.configure('dogpile.cache.dbm', arguments={'filename': '/path/to/cachefile.dbm'})
# scan for videos in the folder and their subtitles
videos = scan_videos(['/path/to/video/folder'], subtitles=True, embedded_subtitles=True)
# download
subliminal.download_best_subtitles(videos, {Language('eng'), Language('fra')}, age=timedelta(week=1))
See :mod:`subliminal.api`, :func:`~subliminal.video.scan_videos` and :func:`~subliminal.video.scan_video`
How it works
------------
Subliminal makes use of various libraries to achieve its goal:
* `enzyme <http://enzyme.readthedocs.org>`_ to detect embedded subtitles in videos and retrieve metadata
* `guessit <http://guessit.readthedocs.org>`_ to guess informations from filenames
* `babelfish <http://babelfish.readthedocs.org>`_ to work with languages
* `requests <http://docs.python-requests.org>`_ to make human readable HTTP requests
* `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup>`_ to parse HTML and XML
* `dogpile.cache <http://dogpilecache.readthedocs.org>`_ to cache intermediate search data
* `charade <https://github.com/sigmavirus24/charade>`_ to detect subtitles' encoding
* `pysrt <https://github.com/byroot/pysrt>`_ to validate downloaded subtitles
License
-------
MIT
Welcome to subliminal!
======================
Subliminal is a python 2.7+ library to search and download subtitles.
It comes with an easy to use yet powerful :abbr:`CLI (command-line interface)` suitable for direct use or cron jobs.
Documentation
@@ -78,25 +14,39 @@ Documentation
.. toctree::
:maxdepth: 2
provider_guide
user/usage
user/how_it_works
user/cli
user/provider_guide
API Documentation
-----------------
If you are looking for information on a specific function, class or method,
this part of the documentation is for you.
If you are looking for information on a specific function, class or method, this part of the documentation is for you.
.. toctree::
:maxdepth: 2
:maxdepth: 1
api/api
api/core
api/video
api/subtitle
api/providers
api/refiners
api/extensions
api/score
api/utils
api/cache
api/cli
api/exceptions
api/providers
api/score
api/subtitle
api/video
.. include:: ../HISTORY.rst
License
-------
MIT
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
+263
View File
@@ -0,0 +1,263 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
: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. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
echo. coverage to run coverage check of the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
REM Check if sphinx-build is available and fallback to Python version if any
%SPHINXBUILD% 2> nul
if errorlevel 9009 goto sphinx_python
goto sphinx_ok
:sphinx_python
set SPHINXBUILD=python -m sphinx.__init__
%SPHINXBUILD% 2> nul
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
:sphinx_ok
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
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.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "coverage" (
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
if errorlevel 1 exit /b 1
echo.
echo.Testing of coverage in the sources finished, look at the ^
results in %BUILDDIR%/coverage/python.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
goto end
)
:end
+18
View File
@@ -0,0 +1,18 @@
.. _cli:
CLI
===
subliminal
----------
.. program-output:: subliminal --help
subliminal download
-------------------
.. program-output:: subliminal download --help
subliminal cache
----------------
.. program-output:: subliminal cache --help
+52
View File
@@ -0,0 +1,52 @@
How it works
============
Providers
---------
Subliminal uses multiple providers to give users a vast choice and have a better chance to find the best matching
subtitles. Current supported providers are:
* Addic7ed
* LegendasTV
* NapiProjekt
* OpenSubtitles
* Podnapisi
* Shooter
* SubsCenter
* TheSubDB
* TvSubtitles
Providers all inherit the same :class:`~subliminal.providers.Provider` base class and thus share the same API.
They are registered on the ``subliminal.providers`` entry point and are exposed through the
:data:`~subliminal.extensions.provider_manager` for easy access.
To work with multiple providers seamlessly, the :class:`~subliminal.core.ProviderPool` exposes the same API but
distributes it to its providers and :class:`~subliminal.core.AsyncProviderPool` does it asynchronously.
.. _scoring:
Scoring
-------
Rating subtitles and comparing them is probably the most difficult part and this is where subliminal excels with its
powerful scoring algorithm.
Using `guessit <http://guessit.readthedocs.org>`_ and `enzyme <http://enzyme.readthedocs.org>`_, subliminal extracts
properties of the video and match them with the properties of the subtitles found with the providers.
Equations in :mod:`subliminal.score` give a score to each property (called a match). The more matches the video and
the subtitle have, the higher the score computed with :func:`~subliminal.score.compute_score` gets.
Libraries
---------
Various libraries are used by subliminal and are key to its success:
* `guessit <http://guessit.readthedocs.org>`_ to guess information from filenames
* `enzyme <http://enzyme.readthedocs.org>`_ to detect embedded subtitles in videos and read other video metadata
* `babelfish <http://babelfish.readthedocs.org>`_ to work with languages
* `requests <http://docs.python-requests.org>`_ to make human readable HTTP requests
* `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup>`_ to parse HTML and XML
* `dogpile.cache <http://dogpilecache.readthedocs.org>`_ to cache intermediate search results
* `stevedore <http://docs.openstack.org/developer/stevedore/>`_ to manage the provider entry point
* `chardet <http://chardet.readthedocs.org>`_ to detect subtitles' encoding
* `pysrt <https://github.com/byroot/pysrt>`_ to validate downloaded subtitles
@@ -1,6 +1,7 @@
Provider Guide
==============
This guide is going to explain how to add a :class:`~subliminal.providers.Provider` to subliminal
This guide is going to explain how to add a :class:`~subliminal.providers.Provider` to subliminal. You are encouraged
to take a look at the existing providers, it can be a nice base to start your own provider.
Requirements
@@ -30,14 +31,15 @@ API keys must not be configurable by the user and must remain linked to sublimin
in the provider module.
Per-user authentication is allowed and must be configured at instantiation as keyword arguments. Configuration
will be done by the user through the `provider_configs` argument of the :func:`~subliminal.api.list_subtitles` and
:func:`~subliminal.api.download_best_subtitles` functions. No network operation must be done during instantiation,
will be done by the user through the `provider_configs` argument of the :func:`~subliminal.core.list_subtitles` and
:func:`~subliminal.core.download_best_subtitles` functions. No network operation must be done during instantiation,
only configuration. Any error in the configuration must raise a
:class:`~subliminal.exceptions.ProviderConfigurationError`.
:class:`~subliminal.exceptions.ConfigurationError`.
Beyond this point, if a network error occurs, a :class:`~subliminal.exceptions.ProviderNotAvailable` exception
must be raised and an unexpected behavior must raise a :class:`~subliminal.exceptions.ProviderError` exception.
Beyond this point, if an error occurs, a generic :class:`~subliminal.exceptions.ProviderError` exception
must be raised. You can also use more explicit exception classes :class:`~subliminal.exceptions.AuthenticationError`
and :class:`~subliminal.exceptions.DownloadLimitExceeded`.
Initialization / Termination
@@ -51,14 +53,16 @@ Caching policy
--------------
To save bandwidth and improve querying time, intermediate data should be cached when possible. Typical use case is
when a query to retrieve show ids is required prior to the query to actually search for subtitles. In that case
the function that gets the show id from the show name must be cached.
the function that gets the show id from the show name must be cached.
Expiration time should be :data:`~subliminal.cache.SHOW_EXPIRATION_TIME` for shows and
:data:`~subliminal.cache.EPISODE_EXPIRATION_TIME` for episodes.
Language
--------
To be able to handle various language codes, subliminal makes use of `babelfish <http://babelfish.readthedocs.org>`_
Language and converters. You must set the attribute :attr:`~subliminal.providers.Provider.languages` with a set of
supported :class:`babelfish.Language`.
supported :class:`~babelfish.language.Language`.
If you cannot find a suitable converter for your provider, you can `make one of your own
<http://babelfish.readthedocs.org/en/latest/#custom-converters>`_.
@@ -67,7 +71,7 @@ If you cannot find a suitable converter for your provider, you can `make one of
Querying
--------
The :meth:`~subliminal.providers.Provider.query` method parameters must include all aspects of provider's querying with
simple types.
primary types.
Subtitle
@@ -79,25 +83,14 @@ It must have relevant attributes that can be used to compute the matches of the
Score computation
-----------------
To be able to compare subtitles coming from different providers between them, the
:meth:`~subliminal.subtitle.Subtitle.compute_matches` method must be implemented.
If `guessit <http://guessit.readthedocs.org>`_ is used to extract data from the
:class:`~subliminal.subtitle.Subtitle` subclass, you can use :func:`~subliminal.subtitle.compute_guess_matches`
as a helper to compute matches between the :class:`~subliminal.video.Video` and the :class:`guessit.Guess`.
Refer to the `scores` attribute of :class:`~subliminal.video.Episode` and :class:`~subliminal.video.Movie`
for a list of possible matches.
:meth:`~subliminal.subtitle.Subtitle.get_matches` method must be implemented.
Unittesting
-----------
All possible uses of the :meth:`~subliminal.providers.Provider.query` method must be unittested including the uses
that produce exceptions other than :class:`~subliminal.exceptions.ProviderNotAvailable`.
The :meth:`~subliminal.subtitle.Subtitle.compute_matches` is used to validate the unittests.
As it is not possible to unittest all uses of the :meth:`~subliminal.providers.Provider.list_subtitles`
and :meth:`~subliminal.providers.Provider.download_subtitle` methods, unitests are only required to cover most common
use cases.
See existing unittests for more details on how to proceed.
All possible uses of :meth:`~subliminal.providers.Provider.query`,
:meth:`~subliminal.providers.Provider.list_subtitles` and :meth:`~subliminal.providers.Provider.download_subtitle`
methods must have integration tests. Use `vcrpy <https://github.com/kevin1024/vcrpy>`_ for recording and playback
of network activity.
Other functions must be unittested. If necessary, you can use :mod:`unittest.mock` to mock some functions.
+148
View File
@@ -0,0 +1,148 @@
Usage
=====
CLI
---
Download English subtitles::
$ subliminal download -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
Collecting videos [####################################] 100%
1 video collected / 0 video ignored
Downloading subtitles [####################################] 100%
Downloaded 1 subtitle
.. warning::
For cron usage, make sure to specify a maximum age (with ``--age``) so subtitles are searched for recent videos
only. Otherwise you will get banned from the providers for abuse due to too many requests. If subliminal didn't
find subtitles for an old video, it's unlikely it will find subtitles for that video ever anyway.
See :ref:`cli` for more details on the available commands and options.
Nautilus/Nemo integration
-------------------------
See the dedicated `project page <https://github.com/Diaoul/nautilus-subliminal>`_ for more information.
High level API
--------------
You can call subliminal in many different ways depending on how much control you want over the process. For most use
cases, you can stick to the standard API.
Common
^^^^^^
Let's start by importing subliminal:
>>> import os
>>> from babelfish import *
>>> from subliminal import *
Before going further, there are a few things to know about subliminal.
Video
^^^^^
The :class:`~subliminal.video.Movie` and :class:`~subliminal.video.Episode` classes represent a video,
existing or not. You can create a video by name (or path) with :meth:`Video.fromname <subliminal.video.Video.fromname>`,
use :func:`~subliminal.core.scan_video` on an existing file path to get even more information about the video or
use :func:`~subliminal.core.scan_videos` on an existing directory path to scan a whole directory for videos.
>>> video = Video.fromname('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4')
>>> video
<Episode ['The Big Bang Theory', 5x18]>
Here video information was guessed based on the name of the video, you can access some video attributes:
>>> video.video_codec
'h264'
>>> video.release_group
'LOL'
Configuration
^^^^^^^^^^^^^
Before proceeding to listing and downloading subtitles, you need to configure the cache. Subliminal uses a cache to
reduce repeated queries to providers and improve overall performance with no impact on search quality. For the sake
of this example, we're going to use a memory backend.
>>> my_region = region.configure('dogpile.cache.memory')
.. warning::
Choose a cache that fits your application and prefer persistent over volatile backends. The ``file`` backend is
usually a good choice.
See `dogpile.cache's documentation <http://dogpilecache.readthedocs.org>`_ for more details on backends.
Now that we're done with the basics, let's have some *real* fun.
Listing
^^^^^^^
To list subtitles, subliminal provides a :func:`~subliminal.core.list_subtitles` function that will return all found
subtitles:
>>> subtitles = list_subtitles([video], {Language('hun')}, providers=['podnapisi'])
>>> subtitles[video]
[<PodnapisiSubtitle 'ZtAW' [hu]>, <PodnapisiSubtitle 'ONAW' [hu]>]
.. note::
As you noticed, all parameters are iterables but only contain one item which means you can deal with a lot of
videos, languages and providers at the same time. For the sake of this example, we filter providers to use only one,
pass ``providers=None`` (default) to search on all providers.
Scoring
^^^^^^^
It's usual you have multiple candidates for subtitles. To help you chose which one to download, subliminal can compare
them to the video and tell you exactly what matches with :meth:`~subliminal.subtitle.Subtitle.get_matches`:
>>> for s in subtitles[video]:
... sorted(s.get_matches(video))
['episode', 'format', 'release_group', 'season', 'series', 'video_codec', 'year']
['episode', 'format', 'season', 'series', 'year']
And then compute a score with those matches with :func:`~subliminal.score.compute_score`:
>>> for s in subtitles[video]:
... {s: compute_score(s, video)}
{<PodnapisiSubtitle 'ZtAW' [hu]>: 354}
{<PodnapisiSubtitle 'ONAW' [hu]>: 337}
Now you should have a better idea about which one you should choose.
Downloading
^^^^^^^^^^^
We can settle on the first subtitle and download its content using :func:`~subliminal.core.download_subtitles`:
>>> subtitle = subtitles[video][0]
>>> subtitle.content is None
True
>>> download_subtitles([subtitle])
>>> subtitle.content.split(b'\n')[2]
b'Elszaladok a boltba'
If you want a string instead of bytes, you can access decoded content with the
:attr:`~subliminal.subtitle.Subtitle.text` property:
>>> subtitle.text.split('\n')[3]
'néhány apróságért.'
Downloading best subtitles
^^^^^^^^^^^^^^^^^^^^^^^^^^
Downloading best subtitles is what you want to do in almost all cases, as a shortcut for listing, scoring and
downloading you can use :func:`~subliminal.core.download_best_subtitles`:
>>> best_subtitles = download_best_subtitles([video], {Language('hun')}, providers=['podnapisi'])
>>> best_subtitles[video]
[<PodnapisiSubtitle 'ZtAW' [hu]>]
>>> best_subtitle = best_subtitles[video][0]
>>> best_subtitle.content.split(b'\n')[2]
b'Elszaladok a boltba'
We end up with the same subtitle but with one line of code. Neat.
Save
^^^^
We got ourselves a nice subtitle, now we can save it on the file system using :func:`~subliminal.core.save_subtitles`:
>>> save_subtitles(video, [best_subtitle])
[<PodnapisiSubtitle 'ZtAW' [hu]>]
>>> os.listdir()
['The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.hu.srt']
+11
View File
@@ -0,0 +1,11 @@
[pytest]
norecursedirs = build dist env .tox .eggs
addopts = --pep8 --flakes --doctest-glob='*.rst'
pep8maxlinelength = 120
pep8ignore =
docs/conf.py ALL
subliminal/__init__.py E402
flakes-ignore =
docs/conf.py ALL
subliminal/__init__.py UnusedImport
doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
+1 -9
View File
@@ -1,9 +1 @@
beautifulsoup4>=4.3.2
guessit>=0.6.2
requests>=2.0.1
enzyme>=0.4.0
html5lib>=0.99
dogpile.cache>=0.5.1
babelfish>=0.3.0
charade>=1.0.3
pysrt>=0.5.0
-e .
+3
View File
@@ -1,3 +1,6 @@
[aliases]
test=pytest
[build_sphinx]
source-dir = docs/
build-dir = docs/_build
+95 -34
View File
@@ -1,40 +1,101 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import io
import os
import re
import sys
from setuptools import setup, find_packages
here = os.path.abspath(os.path.dirname(__file__))
def read(*parts):
# intentionally *not* adding an encoding option to open, See:
# https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690
return io.open(os.path.join(here, *parts), 'r').read()
def find_version(*file_paths):
version_file = read(*file_paths)
version_match = re.search(r'^__version__ = [\'"]([^\'"]*)[\'"]', version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError('Unable to find version string.')
# requirements
setup_requirements = ['pytest-runner'] if {'pytest', 'test', 'ptr'}.intersection(sys.argv) else []
install_requirements = ['guessit>=2.0.1', 'babelfish>=0.5.2', 'enzyme>=0.4.1', 'beautifulsoup4>=4.4.0',
'requests>=2.0', 'click>=4.0', 'dogpile.cache>=0.6.0', 'stevedore>=1.0.0',
'chardet>=2.3.0', 'pysrt>=1.0.1', 'six>=1.9.0', 'appdirs>=1.3', 'rarfile>=2.7',
'pytz>=2012c']
if sys.version_info < (3, 2):
install_requirements.append('futures>=3.0')
test_requirements = ['sympy', 'vcrpy>=1.6.1', 'pytest', 'pytest-pep8', 'pytest-flakes', 'pytest-cov']
if sys.version_info < (3, 3):
test_requirements.append('mock')
dev_requirements = ['tox', 'sphinx', 'sphinx_rtd_theme', 'sphinxcontrib-programoutput', 'wheel']
setup(name='subliminal',
version='0.7.2',
license='MIT',
description='Subtitles, faster than your thoughts',
long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(),
keywords='subtitle subtitles video movie episode tv show',
url='https://github.com/Diaoul/subliminal',
author='Antoine Bertin',
author_email='diaoulael@gmail.com',
packages=find_packages(),
classifiers=['Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Multimedia :: Video'],
entry_points={
'console_scripts': ['subliminal = subliminal.cli:subliminal'],
'subliminal.providers': ['addic7ed = subliminal.providers.addic7ed:Addic7edProvider',
'bierdopje = subliminal.providers.bierdopje:BierDopjeProvider',
'opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider',
'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider',
'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider',
'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider'],
'babelfish.converters': ['addic7ed = subliminal.converters.addic7ed:Addic7edConverter',
'podnapisi = subliminal.converters.podnapisi:PodnapisiConverter',
'tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter']
},
install_requires=open('requirements.txt').readlines(),
test_suite='subliminal.tests.suite')
version=find_version('subliminal', '__init__.py'),
license='MIT',
description='Subtitles, faster than your thoughts',
long_description=read('README.rst') + '\n\n' + read('HISTORY.rst'),
keywords='subtitle subtitles video movie episode tv show series',
url='https://github.com/Diaoul/subliminal',
author='Antoine Bertin',
author_email='diaoulael@gmail.com',
packages=find_packages(),
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Multimedia :: Video'
],
entry_points={
'subliminal.providers': [
'addic7ed = subliminal.providers.addic7ed:Addic7edProvider',
'legendastv = subliminal.providers.legendastv:LegendasTVProvider',
'opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider',
'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider',
'shooter = subliminal.providers.shooter:ShooterProvider',
'subscenter = subliminal.providers.subscenter:SubsCenterProvider',
'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider',
'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider'
],
'subliminal.refiners': [
'metadata = subliminal.refiners.metadata:refine',
'omdb = subliminal.refiners.omdb:refine',
'tvdb = subliminal.refiners.tvdb:refine'
],
'babelfish.language_converters': [
'addic7ed = subliminal.converters.addic7ed:Addic7edConverter',
'shooter = subliminal.converters.shooter:ShooterConverter',
'thesubdb = subliminal.converters.thesubdb:TheSubDBConverter',
'tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter'
],
'console_scripts': [
'subliminal = subliminal.cli:subliminal'
]
},
setup_requires=setup_requirements,
install_requires=install_requirements,
tests_require=test_requirements,
extras_require={
'test': test_requirements,
'dev': dev_requirements
})
+12 -7
View File
@@ -1,16 +1,21 @@
# -*- coding: utf-8 -*-
__title__ = 'subliminal'
__version__ = '0.7.2'
__version__ = '2.0.3'
__short_version__ = '.'.join(__version__.split('.')[:2])
__author__ = 'Antoine Bertin'
__license__ = 'MIT'
__copyright__ = 'Copyright 2013 Antoine Bertin'
__copyright__ = 'Copyright 2016, Antoine Bertin'
import logging
from .api import PROVIDERS_ENTRY_POINT, list_subtitles, download_subtitles, download_best_subtitles
from .cache import region as cache_region
from .exceptions import Error, ProviderError, ProviderConfigurationError, ProviderNotAvailable, InvalidSubtitle
from .subtitle import Subtitle
from .video import VIDEO_EXTENSIONS, SUBTITLE_EXTENSIONS, Video, Episode, Movie, scan_videos, scan_video
from .core import (AsyncProviderPool, ProviderPool, check_video, download_best_subtitles, download_subtitles,
list_subtitles, refine, save_subtitles, scan_video, scan_videos)
from .cache import region
from .exceptions import Error, ProviderError
from .extensions import provider_manager, refiner_manager
from .providers import Provider
from .score import compute_score, get_scores
from .subtitle import SUBTITLE_EXTENSIONS, Subtitle
from .video import VIDEO_EXTENSIONS, Episode, Movie, Video
logging.getLogger(__name__).addHandler(logging.NullHandler())
-276
View File
@@ -1,276 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import collections
import io
import logging
import operator
import babelfish
import pkg_resources
from .exceptions import ProviderNotAvailable, InvalidSubtitle
from .subtitle import get_subtitle_path
logger = logging.getLogger(__name__)
#: Entry point for the providers
PROVIDERS_ENTRY_POINT = 'subliminal.providers'
def list_subtitles(videos, languages, providers=None, provider_configs=None):
"""List subtitles for `videos` with the given `languages` using the specified `providers`
:param videos: videos to list subtitles for
:type videos: set of :class:`~subliminal.video.Video`
:param languages: languages of subtitles to search for
:type languages: set of :class:`babelfish.Language`
:param providers: providers to use for the search, if not all
:type providers: list of string or None
:param provider_configs: configuration for providers
:type provider_configs: dict of provider name => provider constructor kwargs
:return: found subtitles
:rtype: dict of :class:`~subliminal.video.Video` => [:class:`~subliminal.subtitle.Subtitle`]
"""
provider_configs = provider_configs or {}
subtitles = collections.defaultdict(list)
# filter videos
videos = [v for v in videos if v.subtitle_languages & languages < languages]
if not videos:
logger.info('No video to download subtitles for with languages %r', languages)
return subtitles
subtitle_languages = set.intersection(*[v.subtitle_languages for v in videos])
for provider_entry_point in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT):
# filter and initialize provider
if providers is not None and provider_entry_point.name not in providers:
logger.debug('Skipping provider %r: not in the list', provider_entry_point.name)
continue
Provider = provider_entry_point.load()
provider_languages = Provider.languages & languages - subtitle_languages
if not provider_languages:
logger.info('Skipping provider %r: no language to search for', provider_entry_point.name)
continue
provider_videos = [v for v in videos if Provider.check(v)]
if not provider_videos:
logger.info('Skipping provider %r: no video to search for', provider_entry_point.name)
continue
# list subtitles with the provider
try:
with Provider(**provider_configs.get(provider_entry_point.name, {})) as provider:
for provider_video in provider_videos:
provider_video_languages = provider_languages - provider_video.subtitle_languages
if not provider_video_languages:
logger.debug('Skipping provider %r: no language to search for for video %r',
provider_entry_point.name, provider_video)
continue
logger.info('Listing subtitles with provider %r for video %r with languages %r',
provider_entry_point.name, provider_video, provider_video_languages)
try:
provider_subtitles = provider.list_subtitles(provider_video, provider_video_languages)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
break
except:
logger.exception('Unexpected error in provider %r', provider_entry_point.name)
continue
logger.info('Found %d subtitles', len(provider_subtitles))
subtitles[provider_video].extend(provider_subtitles)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
return subtitles
def download_subtitles(subtitles, provider_configs=None, single=False):
"""Download subtitles
:param subtitles: subtitles to download
:type subtitles: dict of :class:`~subliminal.video.Video` => [:class:`~subliminal.subtitle.Subtitle`]
:param provider_configs: configuration for providers
:type provider_configs: dict of provider name => provider constructor kwargs
:param bool single: download with .srt extension if `True`, add language identifier otherwise
"""
provider_configs = provider_configs or {}
discarded_providers = set()
providers_by_name = {ep.name: ep.load() for ep in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT)}
initialized_providers = {}
try:
for video, video_subtitles in subtitles.items():
languages = {subtitle.language for subtitle in video_subtitles}
downloaded_languages = set()
for subtitle in video_subtitles:
# filter
if subtitle.language in downloaded_languages:
continue
if subtitle.provider_name in discarded_providers:
logger.debug('Skipping subtitle from discarded provider %r', subtitle.provider_name)
continue
# initialize provider
if subtitle.provider_name in initialized_providers:
provider = initialized_providers[subtitle.provider_name]
else:
provider = providers_by_name[subtitle.provider_name](**provider_configs.get(subtitle.provider_name, {}))
try:
provider.initialize()
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', subtitle.provider_name)
discarded_providers.add(subtitle.provider_name)
continue
initialized_providers[subtitle.provider_name] = provider
# download subtitles
subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language)
logger.info('Downloading subtitle %r into %r', subtitle, subtitle_path)
try:
subtitle_text = provider.download_subtitle(subtitle)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', subtitle.provider_name)
discarded_providers.add(subtitle.provider_name)
continue
except InvalidSubtitle:
logger.info('Invalid subtitle, skipping it')
continue
except:
logger.exception('Unexpected error in provider %r', subtitle.provider_name)
continue
with io.open(subtitle_path, 'w', encoding='utf-8') as f:
f.write(subtitle_text)
downloaded_languages.add(subtitle.language)
if single or downloaded_languages == languages:
break
finally: # terminate providers
for (provider_name, provider) in initialized_providers.items():
try:
provider.terminate()
except ProviderNotAvailable:
logger.warning('Provider %r is not available, unable to terminate', provider_name)
except:
logger.exception('Unexpected error in provider %r', provider_name)
def download_best_subtitles(videos, languages, providers=None, provider_configs=None, single=False, min_score=0,
hearing_impaired=False):
"""Download the best subtitles for `videos` with the given `languages` using the specified `providers`
:param videos: videos to download subtitles for
:type videos: set of :class:`~subliminal.video.Video`
:param languages: languages of subtitles to download
:type languages: set of :class:`babelfish.Language`
:param providers: providers to use for the search, if not all
:type providers: list of string or None
:param provider_configs: configuration for providers
:type provider_configs: dict of provider name => provider constructor kwargs
:param bool single: download with .srt extension if `True`, add language identifier otherwise
:param int min_score: minimum score for subtitles to download
:param bool hearing_impaired: download hearing impaired subtitles
"""
provider_configs = provider_configs or {}
discarded_providers = set()
downloaded_subtitles = collections.defaultdict(list)
# filter videos
videos = [v for v in videos if v.subtitle_languages & languages < languages
and (not single or babelfish.Language('und') not in v.subtitle_languages)]
if not videos:
logger.info('No video to download subtitles for with languages %r', languages)
return downloaded_subtitles
# filter and initialize providers
subtitle_languages = set.intersection(*[v.subtitle_languages for v in videos])
initialized_providers = {}
for provider_entry_point in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT):
if providers is not None and provider_entry_point.name not in providers:
logger.debug('Skipping provider %r: not in the list', provider_entry_point.name)
continue
Provider = provider_entry_point.load()
if not Provider.languages & languages - subtitle_languages:
logger.info('Skipping provider %r: no language to search for', provider_entry_point.name)
continue
if not [v for v in videos if Provider.check(v)]:
logger.info('Skipping provider %r: no video to search for', provider_entry_point.name)
continue
provider = Provider(**provider_configs.get(provider_entry_point.name, {}))
try:
provider.initialize()
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
continue
initialized_providers[provider_entry_point.name] = provider
try:
for video in videos:
# search for subtitles
subtitles = []
for provider_name, provider in initialized_providers.items():
if provider.check(video):
if provider_name in discarded_providers:
logger.debug('Skipping discarded provider %r', provider_name)
continue
provider_video_languages = provider.languages & languages - video.subtitle_languages
if not provider_video_languages:
logger.debug('Skipping provider %r: no language to search for for video %r', provider_name,
video)
continue
logger.info('Listing subtitles with provider %r for video %r with languages %r',
provider_name, video, provider_video_languages)
try:
provider_subtitles = provider.list_subtitles(video, provider_video_languages)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_name)
discarded_providers.add(provider_name)
continue
except:
logger.exception('Unexpected error in provider %r', provider_name)
continue
logger.info('Found %d subtitles', len(provider_subtitles))
subtitles.extend(provider_subtitles)
# find the best subtitles and download them
downloaded_languages = video.subtitle_languages.copy()
for subtitle, score in sorted([(s, s.compute_score(video)) for s in subtitles],
key=operator.itemgetter(1), reverse=True):
# filter
if subtitle.provider_name in discarded_providers:
logger.debug('Skipping subtitle from discarded provider %r', subtitle.provider_name)
continue
if subtitle.hearing_impaired != hearing_impaired:
logger.debug('Skipping subtitle: hearing impaired != %r', hearing_impaired)
continue
if score < min_score:
logger.debug('Skipping subtitle: score < %d', min_score)
continue
if subtitle.language in downloaded_languages:
logger.debug('Skipping subtitle: %r already downloaded', subtitle.language)
continue
# download
provider = initialized_providers[subtitle.provider_name]
subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language)
logger.info('Downloading subtitle %r with score %d into %r', subtitle, score, subtitle_path)
try:
subtitle_text = provider.download_subtitle(subtitle)
downloaded_subtitles[video].append(subtitle)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', subtitle.provider_name)
discarded_providers.add(subtitle.provider_name)
continue
except InvalidSubtitle:
logger.info('Invalid subtitle, skipping it')
continue
except:
logger.exception('Unexpected error in provider %r', subtitle.provider_name)
continue
with io.open(subtitle_path, 'w', encoding='utf-8') as f:
f.write(subtitle_text)
downloaded_languages.add(subtitle.language)
if single or downloaded_languages >= languages:
logger.debug('All languages downloaded')
break
finally: # terminate providers
for (provider_name, provider) in initialized_providers.items():
try:
provider.terminate()
except ProviderNotAvailable:
logger.warning('Provider %r is not available, unable to terminate', provider_name)
except:
logger.exception('Unexpected error in provider %r', provider_name)
return downloaded_subtitles
+13 -27
View File
@@ -1,30 +1,16 @@
# -*- coding: utf-8 -*-
import inspect
import dogpile.cache
import datetime
from dogpile.cache import make_region
#: Expiration time for show caching
SHOW_EXPIRATION_TIME = datetime.timedelta(weeks=3).total_seconds()
#: Expiration time for episode caching
EPISODE_EXPIRATION_TIME = datetime.timedelta(days=3).total_seconds()
#: Expiration time for scraper searches
REFINER_EXPIRATION_TIME = datetime.timedelta(weeks=1).total_seconds()
#: Subliminal's cache version
CACHE_VERSION = 1
def subliminal_key_generator(namespace, fn, to_str=dogpile.cache.compat.string_type):
"""Add a :data:`CACHE_VERSION` to dogpile.cache's default function_key_generator"""
if namespace is None:
namespace = '%d:%s:%s' % (CACHE_VERSION, fn.__module__, fn.__name__)
else:
namespace = '%d:%s:%s|%s' % (CACHE_VERSION, fn.__module__, fn.__name__, namespace)
args = inspect.getargspec(fn)
has_self = args[0] and args[0][0] in ('self', 'cls')
def generate_key(*args, **kw):
if kw:
raise ValueError('Keyword arguments not supported')
if has_self:
args = args[1:]
return namespace + '|' + ' '.join(map(to_str, args))
return generate_key
#: The dogpile.cache region
region = dogpile.cache.make_region(function_key_generator=subliminal_key_generator)
region = make_region()
+430 -136
View File
@@ -1,167 +1,461 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
import argparse
import datetime
"""
Subliminal uses `click <http://click.pocoo.org>`_ to provide a powerful :abbr:`CLI (command-line interface)`.
"""
from __future__ import division
from collections import defaultdict
from datetime import timedelta
import glob
import json
import logging
import os
import re
import sys
import babelfish
import guessit
import pkg_resources
from subliminal import (__version__, PROVIDERS_ENTRY_POINT, cache_region, Video, Episode, Movie, scan_videos,
download_best_subtitles)
try:
import colorlog
except ImportError:
colorlog = None
from appdirs import AppDirs
from babelfish import Error as BabelfishError, Language
import click
from dogpile.cache.backends.file import AbstractFileLock
from dogpile.util.readwrite_lock import ReadWriteMutex
from six.moves import configparser
from subliminal import (AsyncProviderPool, Episode, Movie, Video, __version__, check_video, compute_score, get_scores,
provider_manager, refine, refiner_manager, region, save_subtitles, scan_video, scan_videos)
from subliminal.core import ARCHIVE_EXTENSIONS, search_external_subtitles
logger = logging.getLogger(__name__)
DEFAULT_CACHE_FILE = os.path.join('~', '.config', 'subliminal.cache.dbm')
class MutexLock(AbstractFileLock):
""":class:`MutexLock` is a thread-based rw lock based on :class:`dogpile.core.ReadWriteMutex`."""
def __init__(self, filename):
self.mutex = ReadWriteMutex()
def acquire_read_lock(self, wait):
ret = self.mutex.acquire_read_lock(wait)
return wait or ret
def acquire_write_lock(self, wait):
ret = self.mutex.acquire_write_lock(wait)
return wait or ret
def release_read_lock(self):
return self.mutex.release_read_lock()
def release_write_lock(self):
return self.mutex.release_write_lock()
def subliminal():
parser = argparse.ArgumentParser(prog='subliminal', description='Subtitles, faster than your thoughts',
epilog='Suggestions and bug reports are greatly appreciated: '
'https://github.com/Diaoul/subliminal/issues', add_help=False)
class Config(object):
"""A :class:`~configparser.ConfigParser` wrapper to store configuration.
# required arguments
required_arguments_group = parser.add_argument_group('required arguments')
required_arguments_group.add_argument('paths', nargs='+', metavar='PATH', help='path to video file or folder')
required_arguments_group.add_argument('-l', '--languages', nargs='+', required=True, metavar='LANGUAGE',
help='wanted languages as IETF codes e.g. fr, pt-BR, sr-Cyrl ')
Interaction with the configuration is done with the properties.
# configuration
configuration_group = parser.add_argument_group('configuration')
configuration_group.add_argument('-s', '--single', action='store_true',
help='download without language code in subtitle\'s filename i.e. .srt only')
configuration_group.add_argument('-c', '--cache-file', default=DEFAULT_CACHE_FILE,
help='cache file (default: %(default)s)')
:param str path: path to the configuration file.
# filtering
filtering_group = parser.add_argument_group('filtering')
providers = [ep.name for ep in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT)]
filtering_group.add_argument('-p', '--providers', nargs='+', metavar='PROVIDER',
help='providers to use (%s)' % ', '.join(providers))
filtering_group.add_argument('-m', '--min-score', type=int,
help='minimum score for subtitles (0-%d for episodes, 0-%d for movies)'
% (Episode.scores['hash'], Movie.scores['hash']))
filtering_group.add_argument('-a', '--age', help='download subtitles for videos newer than AGE e.g. 12h, 1w2d')
filtering_group.add_argument('-h', '--hearing-impaired', action='store_true',
help='download hearing impaired subtitles')
filtering_group.add_argument('-f', '--force', action='store_true',
help='force subtitle download for videos with existing subtitles')
"""
def __init__(self, path):
#: Path to the configuration file
self.path = path
# addic7ed
addic7ed_group = parser.add_argument_group('addic7ed')
addic7ed_group.add_argument('--addic7ed-username', metavar='USERNAME', help='username for addic7ed provider')
addic7ed_group.add_argument('--addic7ed-password', metavar='PASSWORD', help='password for addic7ed provider')
#: The underlying configuration object
self.config = configparser.SafeConfigParser()
self.config.add_section('general')
self.config.set('general', 'languages', json.dumps(['en']))
self.config.set('general', 'providers', json.dumps(sorted([p.name for p in provider_manager])))
self.config.set('general', 'refiners', json.dumps(sorted([r.name for r in refiner_manager])))
self.config.set('general', 'single', str(0))
self.config.set('general', 'embedded_subtitles', str(1))
self.config.set('general', 'age', str(int(timedelta(weeks=2).total_seconds())))
self.config.set('general', 'hearing_impaired', str(1))
self.config.set('general', 'min_score', str(0))
# output
output_group = parser.add_argument_group('output')
output_exclusive_group = output_group.add_mutually_exclusive_group()
output_exclusive_group.add_argument('-q', '--quiet', action='store_true', help='disable output')
output_exclusive_group.add_argument('-v', '--verbose', action='store_true', help='verbose output')
output_group.add_argument('--color', action='store_true', help='add color to console output (requires colorlog)')
def read(self):
"""Read the configuration from :attr:`path`"""
self.config.read(self.path)
# troubleshooting
troubleshooting_group = parser.add_argument_group('troubleshooting')
troubleshooting_group.add_argument('--debug', action='store_true', help='debug output')
troubleshooting_group.add_argument('--version', action='version', version=__version__)
troubleshooting_group.add_argument('--help', action='help', help='show this help message and exit')
def write(self):
"""Write the configuration to :attr:`path`"""
with open(self.path, 'w') as f:
self.config.write(f)
# parse args
args = parser.parse_args()
@property
def languages(self):
return {Language.fromietf(l) for l in json.loads(self.config.get('general', 'languages'))}
# parse paths
try:
args.paths = [os.path.abspath(os.path.expanduser(p.decode('utf-8'))) for p in args.paths]
except UnicodeDecodeError:
parser.error('argument paths: encodings is not utf-8: %r' % args.paths)
@languages.setter
def languages(self, value):
self.config.set('general', 'languages', json.dumps(sorted([str(l) for l in value])))
# parse languages
try:
args.languages = {babelfish.Language.fromietf(l) for l in args.languages}
except babelfish.Error:
parser.error('argument -l/--languages: codes are not IETF: %r' % args.languages)
@property
def providers(self):
return json.loads(self.config.get('general', 'providers'))
# parse age
if args.age is not None:
match = re.match(r'^(?:(?P<weeks>\d+?)w)?(?:(?P<days>\d+?)d)?(?:(?P<hours>\d+?)h)?$', args.age)
@providers.setter
def providers(self, value):
self.config.set('general', 'providers', json.dumps(sorted([p.lower() for p in value])))
@property
def refiners(self):
return json.loads(self.config.get('general', 'refiners'))
@refiners.setter
def refiners(self, value):
self.config.set('general', 'refiners', json.dumps([r.lower() for r in value]))
@property
def single(self):
return self.config.getboolean('general', 'single')
@single.setter
def single(self, value):
self.config.set('general', 'single', str(int(value)))
@property
def embedded_subtitles(self):
return self.config.getboolean('general', 'embedded_subtitles')
@embedded_subtitles.setter
def embedded_subtitles(self, value):
self.config.set('general', 'embedded_subtitles', str(int(value)))
@property
def age(self):
return timedelta(seconds=self.config.getint('general', 'age'))
@age.setter
def age(self, value):
self.config.set('general', 'age', str(int(value.total_seconds())))
@property
def hearing_impaired(self):
return self.config.getboolean('general', 'hearing_impaired')
@hearing_impaired.setter
def hearing_impaired(self, value):
self.config.set('general', 'hearing_impaired', str(int(value)))
@property
def min_score(self):
return self.config.getfloat('general', 'min_score')
@min_score.setter
def min_score(self, value):
self.config.set('general', 'min_score', str(value))
@property
def provider_configs(self):
rv = {}
for provider in provider_manager:
if self.config.has_section(provider.name):
rv[provider.name] = {k: v for k, v in self.config.items(provider.name)}
return rv
@provider_configs.setter
def provider_configs(self, value):
# loop over provider configurations
for provider, config in value.items():
# create the corresponding section if necessary
if not self.config.has_section(provider):
self.config.add_section(provider)
# add config options
for k, v in config.items():
self.config.set(provider, k, v)
class LanguageParamType(click.ParamType):
""":class:`~click.ParamType` for languages that returns a :class:`~babelfish.language.Language`"""
name = 'language'
def convert(self, value, param, ctx):
try:
return Language.fromietf(value)
except BabelfishError:
self.fail('%s is not a valid language' % value)
LANGUAGE = LanguageParamType()
class AgeParamType(click.ParamType):
""":class:`~click.ParamType` for age strings that returns a :class:`~datetime.timedelta`
An age string is in the form `number + identifier` with possible identifiers:
* ``w`` for weeks
* ``d`` for days
* ``h`` for hours
The form can be specified multiple times but only with that idenfier ordering. For example:
* ``1w2d4h`` for 1 week, 2 days and 4 hours
* ``2w`` for 2 weeks
* ``3w6h`` for 3 weeks and 6 hours
"""
name = 'age'
def convert(self, value, param, ctx):
match = re.match(r'^(?:(?P<weeks>\d+?)w)?(?:(?P<days>\d+?)d)?(?:(?P<hours>\d+?)h)?$', value)
if not match:
parser.error('argument -a/--age: invalid age: %r' % args.age)
args.age = datetime.timedelta(**{k: int(v) for k, v in match.groupdict(0).items()})
self.fail('%s is not a valid age' % value)
# parse cache-file
args.cache_file = os.path.abspath(os.path.expanduser(args.cache_file))
if not os.path.exists(os.path.split(args.cache_file)[0]):
parser.error('argument -c/--cache-file: directory %r for cache file does not exist'
% os.path.split(args.cache_file)[0])
return timedelta(**{k: int(v) for k, v in match.groupdict(0).items()})
# parse provider configs
provider_configs = {}
if (args.addic7ed_username is not None and args.addic7ed_password is None
or args.addic7ed_username is None and args.addic7ed_password is not None):
parser.error('argument --addic7ed-username/--addic7ed-password: both arguments are required or none')
if args.addic7ed_username is not None and args.addic7ed_password is not None:
provider_configs['addic7ed'] = {'username': args.addic7ed_username, 'password': args.addic7ed_password}
AGE = AgeParamType()
# parse color
if args.color and colorlog is None:
parser.error('argument --color: colorlog required')
PROVIDER = click.Choice(sorted(provider_manager.names()))
# setup output
if args.debug:
handler = logging.StreamHandler()
if args.color:
handler.setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)-8s%(reset)s [%(blue)s%(name)s-%(funcName)s:%(lineno)d%(reset)s] %(message)s',
log_colors=dict(colorlog.default_log_colors.items() + [('DEBUG', 'cyan')])))
else:
handler.setFormatter(logging.Formatter('%(levelname)-8s [%(name)s-%(funcName)s:%(lineno)d] %(message)s'))
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.DEBUG)
elif args.verbose:
handler = logging.StreamHandler()
if args.color:
handler.setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)-8s%(reset)s [%(blue)s%(name)s%(reset)s] %(message)s'))
else:
handler.setFormatter(logging.Formatter('%(levelname)-8s [%(name)s] %(message)s'))
logging.getLogger('subliminal').addHandler(handler)
logging.getLogger('subliminal').setLevel(logging.INFO)
elif not args.quiet:
handler = logging.StreamHandler()
if args.color:
handler.setFormatter(colorlog.ColoredFormatter('[%(log_color)s%(levelname)s%(reset)s] %(message)s'))
else:
handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
logging.getLogger('subliminal.api').addHandler(handler)
logging.getLogger('subliminal.api').setLevel(logging.INFO)
REFINER = click.Choice(sorted(refiner_manager.names()))
dirs = AppDirs('subliminal')
cache_file = 'subliminal.dbm'
config_file = 'config.ini'
@click.group(context_settings={'max_content_width': 100}, epilog='Suggestions and bug reports are greatly appreciated: '
'https://github.com/Diaoul/subliminal/')
@click.option('--addic7ed', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD', help='Addic7ed configuration.')
@click.option('--legendastv', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD', help='LegendasTV configuration.')
@click.option('--opensubtitles', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD',
help='OpenSubtitles configuration.')
@click.option('--subscenter', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD', help='SubsCenter configuration.')
@click.option('--cache-dir', type=click.Path(writable=True, file_okay=False), default=dirs.user_cache_dir,
show_default=True, expose_value=True, help='Path to the cache directory.')
@click.option('--debug', is_flag=True, help='Print useful information for debugging subliminal and for reporting bugs.')
@click.version_option(__version__)
@click.pass_context
def subliminal(ctx, addic7ed, legendastv, opensubtitles, subscenter, cache_dir, debug):
"""Subtitles, faster than your thoughts."""
# create cache directory
try:
os.makedirs(cache_dir)
except OSError:
if not os.path.isdir(cache_dir):
raise
# configure cache
cache_region.configure('dogpile.cache.dbm', expiration_time=datetime.timedelta(days=30),
arguments={'filename': args.cache_file})
region.configure('dogpile.cache.dbm', expiration_time=timedelta(days=30),
arguments={'filename': os.path.join(cache_dir, cache_file), 'lock_factory': MutexLock})
# configure logging
if debug:
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
logging.getLogger('subliminal').addHandler(handler)
logging.getLogger('subliminal').setLevel(logging.DEBUG)
# provider configs
ctx.obj = {'provider_configs': {}}
if addic7ed:
ctx.obj['provider_configs']['addic7ed'] = {'username': addic7ed[0], 'password': addic7ed[1]}
if legendastv:
ctx.obj['provider_configs']['legendastv'] = {'username': legendastv[0], 'password': legendastv[1]}
if opensubtitles:
ctx.obj['provider_configs']['opensubtitles'] = {'username': opensubtitles[0], 'password': opensubtitles[1]}
if subscenter:
ctx.obj['provider_configs']['subscenter'] = {'username': subscenter[0], 'password': subscenter[1]}
@subliminal.command()
@click.option('--clear-subliminal', is_flag=True, help='Clear subliminal\'s cache. Use this ONLY if your cache is '
'corrupted or if you experience issues.')
@click.pass_context
def cache(ctx, clear_subliminal):
"""Cache management."""
if clear_subliminal:
for file in glob.glob(os.path.join(ctx.parent.params['cache_dir'], cache_file) + '*'):
os.remove(file)
click.echo('Subliminal\'s cache cleared.')
else:
click.echo('Nothing done.')
@subliminal.command()
@click.option('-l', '--language', type=LANGUAGE, required=True, multiple=True, help='Language as IETF code, '
'e.g. en, pt-BR (can be used multiple times).')
@click.option('-p', '--provider', type=PROVIDER, multiple=True, help='Provider to use (can be used multiple times).')
@click.option('-r', '--refiner', type=REFINER, multiple=True, help='Refiner to use (can be used multiple times).')
@click.option('-a', '--age', type=AGE, help='Filter videos newer than AGE, e.g. 12h, 1w2d.')
@click.option('-d', '--directory', type=click.STRING, metavar='DIR', help='Directory where to save subtitles, '
'default is next to the video file.')
@click.option('-e', '--encoding', type=click.STRING, metavar='ENC', help='Subtitle file encoding, default is to '
'preserve original encoding.')
@click.option('-s', '--single', is_flag=True, default=False, help='Save subtitle without language code in the file '
'name, i.e. use .srt extension. Do not use this unless your media player requires it.')
@click.option('-f', '--force', is_flag=True, default=False, help='Force download even if a subtitle already exist.')
@click.option('-hi', '--hearing-impaired', is_flag=True, default=False, help='Prefer hearing impaired subtitles.')
@click.option('-m', '--min-score', type=click.IntRange(0, 100), default=0, help='Minimum score for a subtitle '
'to be downloaded (0 to 100).')
@click.option('-w', '--max-workers', type=click.IntRange(1, 50), default=None, help='Maximum number of threads to use.')
@click.option('-z/-Z', '--archives/--no-archives', default=True, show_default=True, help='Scan archives for videos '
'(supported extensions: %s).' % ', '.join(ARCHIVE_EXTENSIONS))
@click.option('-v', '--verbose', count=True, help='Increase verbosity.')
@click.argument('path', type=click.Path(), required=True, nargs=-1)
@click.pass_obj
def download(obj, provider, refiner, language, age, directory, encoding, single, force, hearing_impaired, min_score,
max_workers, archives, verbose, path):
"""Download best subtitles.
PATH can be an directory containing videos, a video file path or a video file name. It can be used multiple times.
If an existing subtitle is detected (external or embedded) in the correct language, the download is skipped for
the associated video.
"""
# process parameters
language = set(language)
# scan videos
videos = scan_videos([p for p in args.paths if os.path.exists(p)], subtitles=not args.force,
embedded_subtitles=not args.force, age=args.age)
videos = []
ignored_videos = []
errored_paths = []
with click.progressbar(path, label='Collecting videos', item_show_func=lambda p: p or '') as bar:
for p in bar:
logger.debug('Collecting path %s', p)
# guess videos
videos.extend([Video.fromguess(os.path.split(p)[1], guessit.guess_file_info(p, 'autodetect')) for p in args.paths
if not os.path.exists(p)])
# non-existing
if not os.path.exists(p):
try:
video = Video.fromname(p)
except:
logger.exception('Unexpected error while collecting non-existing path %s', p)
errored_paths.append(p)
continue
if not force:
video.subtitle_languages |= set(search_external_subtitles(video.name, directory=directory).values())
refine(video, episode_refiners=refiner, movie_refiners=refiner, embedded_subtitles=not force)
videos.append(video)
continue
# directories
if os.path.isdir(p):
try:
scanned_videos = scan_videos(p, age=age, archives=archives)
except:
logger.exception('Unexpected error while collecting directory path %s', p)
errored_paths.append(p)
continue
for video in scanned_videos:
if check_video(video, languages=language, age=age, undefined=single):
if not force:
video.subtitle_languages |= set(search_external_subtitles(video.name,
directory=directory).values())
refine(video, episode_refiners=refiner, movie_refiners=refiner, embedded_subtitles=not force)
videos.append(video)
else:
ignored_videos.append(video)
continue
# other inputs
try:
video = scan_video(p)
except:
logger.exception('Unexpected error while collecting path %s', p)
errored_paths.append(p)
continue
if check_video(video, languages=language, age=age, undefined=single):
if not force:
video.subtitle_languages |= set(search_external_subtitles(video.name, directory=directory).values())
refine(video, episode_refiners=refiner, movie_refiners=refiner, embedded_subtitles=not force)
videos.append(video)
else:
ignored_videos.append(video)
# output errored paths
if verbose > 0:
for p in errored_paths:
click.secho('%s errored' % p, fg='red')
# output ignored videos
if verbose > 1:
for video in ignored_videos:
click.secho('%s ignored - subtitles: %s / age: %d day%s' % (
os.path.split(video.name)[1],
', '.join(str(s) for s in video.subtitle_languages) or 'none',
video.age.days,
's' if video.age.days > 1 else ''
), fg='yellow')
# report collected videos
click.echo('%s video%s collected / %s video%s ignored / %s error%s' % (
click.style(str(len(videos)), bold=True, fg='green' if videos else None),
's' if len(videos) > 1 else '',
click.style(str(len(ignored_videos)), bold=True, fg='yellow' if ignored_videos else None),
's' if len(ignored_videos) > 1 else '',
click.style(str(len(errored_paths)), bold=True, fg='red' if errored_paths else None),
's' if len(errored_paths) > 1 else '',
))
# exit if no video collected
if not videos:
return
# download best subtitles
subtitles = download_best_subtitles(videos, args.languages, providers=args.providers,
provider_configs=provider_configs, single=args.single,
min_score=args.min_score, hearing_impaired=args.hearing_impaired)
downloaded_subtitles = defaultdict(list)
with AsyncProviderPool(max_workers=max_workers, providers=provider, provider_configs=obj['provider_configs']) as p:
with click.progressbar(videos, label='Downloading subtitles',
item_show_func=lambda v: os.path.split(v.name)[1] if v is not None else '') as bar:
for v in bar:
scores = get_scores(v)
subtitles = p.download_best_subtitles(p.list_subtitles(v, language - v.subtitle_languages),
v, language, min_score=scores['hash'] * min_score / 100,
hearing_impaired=hearing_impaired, only_one=single)
downloaded_subtitles[v] = subtitles
# result output
if not subtitles:
if not args.quiet:
sys.stderr.write('No subtitles downloaded\n')
exit(1)
if not args.quiet:
subtitles_count = sum([len(s) for s in subtitles.values()])
if subtitles_count == 1:
print('%d subtitle downloaded' % subtitles_count)
else:
print('%d subtitles downloaded' % subtitles_count)
if p.discarded_providers:
click.secho('Some providers have been discarded due to unexpected errors: %s' %
', '.join(p.discarded_providers), fg='yellow')
# save subtitles
total_subtitles = 0
for v, subtitles in downloaded_subtitles.items():
saved_subtitles = save_subtitles(v, subtitles, single=single, directory=directory, encoding=encoding)
total_subtitles += len(saved_subtitles)
if verbose > 0:
click.echo('%s subtitle%s downloaded for %s' % (click.style(str(len(saved_subtitles)), bold=True),
's' if len(saved_subtitles) > 1 else '',
os.path.split(v.name)[1]))
if verbose > 1:
for s in saved_subtitles:
matches = s.get_matches(v)
score = compute_score(s, v)
# score color
score_color = None
scores = get_scores(v)
if isinstance(v, Movie):
if score < scores['title']:
score_color = 'red'
elif score < scores['title'] + scores['year'] + scores['release_group']:
score_color = 'yellow'
else:
score_color = 'green'
elif isinstance(v, Episode):
if score < scores['series'] + scores['season'] + scores['episode']:
score_color = 'red'
elif score < scores['series'] + scores['season'] + scores['episode'] + scores['release_group']:
score_color = 'yellow'
else:
score_color = 'green'
# scale score from 0 to 100 taking out preferences
scaled_score = score
if s.hearing_impaired == hearing_impaired:
scaled_score -= scores['hearing_impaired']
scaled_score *= 100 / scores['hash']
# echo some nice colored output
click.echo(' - [{score}] {language} subtitle from {provider_name} (match on {matches})'.format(
score=click.style('{:5.1f}'.format(scaled_score), fg=score_color, bold=score >= scores['hash']),
language=s.language.name if s.language.country is None else '%s (%s)' % (s.language.name,
s.language.country.name),
provider_name=s.provider_name,
matches=', '.join(sorted(matches, key=scores.get, reverse=True))
))
if verbose == 0:
click.echo('Downloaded %s subtitle%s' % (click.style(str(total_subtitles), bold=True),
's' if total_subtitles > 1 else ''))
+9 -8
View File
@@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from babelfish.converters.name import NameConverter
from babelfish import LanguageReverseConverter, language_converters
class Addic7edConverter(NameConverter):
class Addic7edConverter(LanguageReverseConverter):
def __init__(self):
super(Addic7edConverter, self).__init__()
self.from_addic7ed = {'Català': ('cat',), 'Chinese (Simplified)': ('zho',), 'Chinese (Traditional)': ('zho',),
self.name_converter = language_converters['name']
self.from_addic7ed = {u'Català': ('cat',), 'Chinese (Simplified)': ('zho',), 'Chinese (Traditional)': ('zho',),
'Euskera': ('eus',), 'Galego': ('glg',), 'Greek': ('ell',), 'Malay': ('msa',),
'Portuguese (Brazilian)': ('por', 'BR'), 'Serbian (Cyrillic)': ('srp', None, 'Cyrl'),
'Serbian (Latin)': ('srp',), 'Spanish (Latin America)': ('spa',),
@@ -14,7 +13,7 @@ class Addic7edConverter(NameConverter):
self.to_addic7ed = {('cat',): 'Català', ('zho',): 'Chinese (Simplified)', ('eus',): 'Euskera',
('glg',): 'Galego', ('ell',): 'Greek', ('msa',): 'Malay',
('por', 'BR'): 'Portuguese (Brazilian)', ('srp', None, 'Cyrl'): 'Serbian (Cyrillic)'}
self.codes |= set(self.from_addic7ed.keys())
self.codes = self.name_converter.codes | set(self.from_addic7ed.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3, country, script) in self.to_addic7ed:
@@ -23,9 +22,11 @@ class Addic7edConverter(NameConverter):
return self.to_addic7ed[(alpha3, country)]
if (alpha3,) in self.to_addic7ed:
return self.to_addic7ed[(alpha3,)]
return super(Addic7edConverter, self).convert(alpha3, country, script)
return self.name_converter.convert(alpha3, country, script)
def reverse(self, addic7ed):
if addic7ed in self.from_addic7ed:
return self.from_addic7ed[addic7ed]
return super(Addic7edConverter, self).reverse(addic7ed)
return self.name_converter.reverse(addic7ed)
+27
View File
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from babelfish import LanguageReverseConverter
from ..exceptions import ConfigurationError
class LegendasTVConverter(LanguageReverseConverter):
def __init__(self):
self.from_legendastv = {1: ('por', 'BR'), 2: ('eng',), 3: ('spa',), 4: ('fra',), 5: ('deu',), 6: ('jpn',),
7: ('dan',), 8: ('nor',), 9: ('swe',), 10: ('por',), 11: ('ara',), 12: ('ces',),
13: ('zho',), 14: ('kor',), 15: ('bul',), 16: ('ita',), 17: ('pol',)}
self.to_legendastv = {v: k for k, v in self.from_legendastv.items()}
self.codes = set(self.from_legendastv.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3, country) in self.to_legendastv:
return self.to_legendastv[(alpha3, country)]
if (alpha3,) in self.to_legendastv:
return self.to_legendastv[(alpha3,)]
raise ConfigurationError('Unsupported language code for legendastv: %s, %s, %s' % (alpha3, country, script))
def reverse(self, legendastv):
if legendastv in self.from_legendastv:
return self.from_legendastv[legendastv]
raise ConfigurationError('Unsupported language number for legendastv: %s' % legendastv)
-33
View File
@@ -1,33 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from babelfish import ReverseConverter, ConvertError, ReverseError
class PodnapisiConverter(ReverseConverter):
def __init__(self):
super(PodnapisiConverter, self).__init__()
self.from_podnapisi = {2: ('eng',), 28: ('spa',), 26: ('pol',), 36: ('srp',), 1: ('slv',), 38: ('hrv',),
9: ('ita',), 8: ('fra',), 48: ('por', 'BR'), 23: ('nld',), 12: ('ara',), 13: ('ron',),
33: ('bul',), 32: ('por',), 16: ('ell',), 15: ('hun',), 31: ('fin',), 30: ('tur',),
7: ('ces',), 25: ('swe',), 27: ('rus',), 24: ('dan',), 22: ('heb',), 51: ('vie',),
52: ('fas',), 5: ('deu',), 14: ('spa', 'AR'), 54: ('ind',), 47: ('srp', None, 'Cyrl'),
3: ('nor',), 20: ('est',), 10: ('bos',), 17: ('zho',), 37: ('slk',), 35: ('mkd',),
11: ('jpn',), 4: ('kor',), 29: ('sqi',), 6: ('isl',), 19: ('lit',), 46: ('ukr',),
44: ('tha',), 53: ('cat',), 56: ('sin',), 21: ('lav',), 40: ('cmn',), 55: ('msa',),
42: ('hin',), 50: ('bel',)}
self.to_podnapisi = {v: k for k, v in self.from_podnapisi.items()}
self.codes = set(self.from_podnapisi.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3,) in self.to_podnapisi:
return self.to_podnapisi[(alpha3,)]
if (alpha3, country) in self.to_podnapisi:
return self.to_podnapisi[(alpha3, country)]
if (alpha3, country, script) in self.to_podnapisi:
return self.to_podnapisi[(alpha3, country, script)]
raise ConvertError(alpha3, country, script)
def reverse(self, podnapisi):
if podnapisi not in self.from_podnapisi:
raise ReverseError(podnapisi)
return self.from_podnapisi[podnapisi]
+23
View File
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from babelfish import LanguageReverseConverter
from ..exceptions import ConfigurationError
class ShooterConverter(LanguageReverseConverter):
def __init__(self):
self.from_shooter = {'chn': ('zho',), 'eng': ('eng',)}
self.to_shooter = {v: k for k, v in self.from_shooter.items()}
self.codes = set(self.from_shooter.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3,) in self.to_shooter:
return self.to_shooter[(alpha3,)]
raise ConfigurationError('Unsupported language for shooter: %s, %s, %s' % (alpha3, country, script))
def reverse(self, shooter):
if shooter in self.from_shooter:
return self.from_shooter[shooter]
raise ConfigurationError('Unsupported language code for shooter: %s' % shooter)
+26
View File
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from babelfish import LanguageReverseConverter
from ..exceptions import ConfigurationError
class TheSubDBConverter(LanguageReverseConverter):
def __init__(self):
self.from_thesubdb = {'en': ('eng',), 'es': ('spa',), 'fr': ('fra',), 'it': ('ita',), 'nl': ('nld',),
'pl': ('pol',), 'pt': ('por', 'BR'), 'ro': ('ron',), 'sv': ('swe',), 'tr': ('tur',)}
self.to_thesubdb = {v: k for k, v in self.from_thesubdb.items()}
self.codes = set(self.from_thesubdb.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3, country) in self.to_thesubdb:
return self.to_thesubdb[(alpha3, country)]
if (alpha3,) in self.to_thesubdb:
return self.to_thesubdb[(alpha3,)]
raise ConfigurationError('Unsupported language for thesubdb: %s, %s, %s' % (alpha3, country, script))
def reverse(self, thesubdb):
if thesubdb in self.from_thesubdb:
return self.from_thesubdb[thesubdb]
raise ConfigurationError('Unsupported language code for thesubdb: %s' % thesubdb)
+9 -8
View File
@@ -1,24 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from babelfish.converters.alpha2 import Alpha2Converter
from babelfish import LanguageReverseConverter, language_converters
class TVsubtitlesConverter(Alpha2Converter):
class TVsubtitlesConverter(LanguageReverseConverter):
def __init__(self):
super(TVsubtitlesConverter, self).__init__()
self.alpha2_converter = language_converters['alpha2']
self.from_tvsubtitles = {'br': ('por', 'BR'), 'ua': ('ukr',), 'gr': ('ell',), 'cn': ('zho',), 'jp': ('jpn',),
'cz': ('ces',)}
self.to_tvsubtitles = {v: k for k, v in self.from_tvsubtitles}
self.codes |= set(self.from_tvsubtitles.keys())
self.to_tvsubtitles = {v: k for k, v in self.from_tvsubtitles.items()}
self.codes = self.alpha2_converter.codes | set(self.from_tvsubtitles.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3, country) in self.to_tvsubtitles:
return self.to_tvsubtitles[(alpha3, country)]
if (alpha3,) in self.to_tvsubtitles:
return self.to_tvsubtitles[(alpha3,)]
return super(TVsubtitlesConverter, self).convert(alpha3, country, script)
return self.alpha2_converter.convert(alpha3, country, script)
def reverse(self, tvsubtitles):
if tvsubtitles in self.from_tvsubtitles:
return self.from_tvsubtitles[tvsubtitles]
return super(TVsubtitlesConverter, self).reverse(tvsubtitles)
return self.alpha2_converter.reverse(tvsubtitles)
+705
View File
@@ -0,0 +1,705 @@
# -*- coding: utf-8 -*-
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
import io
import itertools
import logging
import operator
import os.path
import socket
from babelfish import Language, LanguageReverseError
from guessit import guessit
from rarfile import NotRarFile, RarCannotExec, RarFile
import requests
from .extensions import provider_manager, refiner_manager
from .score import compute_score as default_compute_score
from .subtitle import SUBTITLE_EXTENSIONS, get_subtitle_path
from .utils import hash_napiprojekt, hash_opensubtitles, hash_shooter, hash_thesubdb
from .video import VIDEO_EXTENSIONS, Episode, Movie, Video
#: Supported archive extensions
ARCHIVE_EXTENSIONS = ('.rar',)
logger = logging.getLogger(__name__)
class ProviderPool(object):
"""A pool of providers with the same API as a single :class:`~subliminal.providers.Provider`.
It has a few extra features:
* Lazy loads providers when needed and supports the `with` statement to :meth:`terminate`
the providers on exit.
* Automatically discard providers on failure.
:param list providers: name of providers to use, if not all.
:param dict provider_configs: provider configuration as keyword arguments per provider name to pass when
instanciating the :class:`~subliminal.providers.Provider`.
"""
def __init__(self, providers=None, provider_configs=None):
#: Name of providers to use
self.providers = providers or provider_manager.names()
#: Provider configuration
self.provider_configs = provider_configs or {}
#: Initialized providers
self.initialized_providers = {}
#: Discarded providers
self.discarded_providers = set()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.terminate()
def __getitem__(self, name):
if name not in self.providers:
raise KeyError
if name not in self.initialized_providers:
logger.info('Initializing provider %s', name)
provider = provider_manager[name].plugin(**self.provider_configs.get(name, {}))
provider.initialize()
self.initialized_providers[name] = provider
return self.initialized_providers[name]
def __delitem__(self, name):
if name not in self.initialized_providers:
raise KeyError(name)
try:
logger.info('Terminating provider %s', name)
self.initialized_providers[name].terminate()
except (requests.Timeout, socket.timeout):
logger.error('Provider %r timed out, improperly terminated', name)
except:
logger.exception('Provider %r terminated unexpectedly', name)
del self.initialized_providers[name]
def __iter__(self):
return iter(self.initialized_providers)
def list_subtitles_provider(self, provider, video, languages):
"""List subtitles with a single provider.
The video and languages are checked against the provider.
:param str provider: name of the provider.
:param video: video to list subtitles for.
:type video: :class:`~subliminal.video.Video`
:param languages: languages to search for.
:type languages: set of :class:`~babelfish.language.Language`
:return: found subtitles.
:rtype: list of :class:`~subliminal.subtitle.Subtitle` or None
"""
# check video validity
if not provider_manager[provider].plugin.check(video):
logger.info('Skipping provider %r: not a valid video', provider)
return []
# check supported languages
provider_languages = provider_manager[provider].plugin.languages & languages
if not provider_languages:
logger.info('Skipping provider %r: no language to search for', provider)
return []
# list subtitles
logger.info('Listing subtitles with provider %r and languages %r', provider, provider_languages)
try:
return self[provider].list_subtitles(video, provider_languages)
except (requests.Timeout, socket.timeout):
logger.error('Provider %r timed out', provider)
except:
logger.exception('Unexpected error in provider %r', provider)
def list_subtitles(self, video, languages):
"""List subtitles.
:param video: video to list subtitles for.
:type video: :class:`~subliminal.video.Video`
:param languages: languages to search for.
:type languages: set of :class:`~babelfish.language.Language`
:return: found subtitles.
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
"""
subtitles = []
for name in self.providers:
# check discarded providers
if name in self.discarded_providers:
logger.debug('Skipping discarded provider %r', name)
continue
# list subtitles
provider_subtitles = self.list_subtitles_provider(name, video, languages)
if provider_subtitles is None:
logger.info('Discarding provider %s', name)
self.discarded_providers.add(name)
continue
# add the subtitles
subtitles.extend(provider_subtitles)
return subtitles
def download_subtitle(self, subtitle):
"""Download `subtitle`'s :attr:`~subliminal.subtitle.Subtitle.content`.
:param subtitle: subtitle to download.
:type subtitle: :class:`~subliminal.subtitle.Subtitle`
:return: `True` if the subtitle has been successfully downloaded, `False` otherwise.
:rtype: bool
"""
# check discarded providers
if subtitle.provider_name in self.discarded_providers:
logger.warning('Provider %r is discarded', subtitle.provider_name)
return False
logger.info('Downloading subtitle %r', subtitle)
try:
self[subtitle.provider_name].download_subtitle(subtitle)
except (requests.Timeout, socket.timeout):
logger.error('Provider %r timed out, discarding it', subtitle.provider_name)
self.discarded_providers.add(subtitle.provider_name)
return False
except:
logger.exception('Unexpected error in provider %r, discarding it', subtitle.provider_name)
self.discarded_providers.add(subtitle.provider_name)
return False
# check subtitle validity
if not subtitle.is_valid():
logger.error('Invalid subtitle')
return False
return True
def download_best_subtitles(self, subtitles, video, languages, min_score=0, hearing_impaired=False, only_one=False,
compute_score=None):
"""Download the best matching subtitles.
:param subtitles: the subtitles to use.
:type subtitles: list of :class:`~subliminal.subtitle.Subtitle`
:param video: video to download subtitles for.
:type video: :class:`~subliminal.video.Video`
:param languages: languages to download.
:type languages: set of :class:`~babelfish.language.Language`
:param int min_score: minimum score for a subtitle to be downloaded.
:param bool hearing_impaired: hearing impaired preference.
:param bool only_one: download only one subtitle, not one per language.
:param compute_score: function that takes `subtitle` and `video` as positional arguments,
`hearing_impaired` as keyword argument and returns the score.
:return: downloaded subtitles.
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
"""
compute_score = compute_score or default_compute_score
# sort subtitles by score
scored_subtitles = sorted([(s, compute_score(s, video, hearing_impaired=hearing_impaired))
for s in subtitles], key=operator.itemgetter(1), reverse=True)
# download best subtitles, falling back on the next on error
downloaded_subtitles = []
for subtitle, score in scored_subtitles:
# check score
if score < min_score:
logger.info('Score %d is below min_score (%d)', score, min_score)
break
# check downloaded languages
if subtitle.language in set(s.language for s in downloaded_subtitles):
logger.debug('Skipping subtitle: %r already downloaded', subtitle.language)
continue
# download
if self.download_subtitle(subtitle):
downloaded_subtitles.append(subtitle)
# stop when all languages are downloaded
if set(s.language for s in downloaded_subtitles) == languages:
logger.debug('All languages downloaded')
break
# stop if only one subtitle is requested
if only_one:
logger.debug('Only one subtitle downloaded')
break
return downloaded_subtitles
def terminate(self):
"""Terminate all the :attr:`initialized_providers`."""
logger.debug('Terminating initialized providers')
for name in list(self.initialized_providers):
del self[name]
class AsyncProviderPool(ProviderPool):
"""Subclass of :class:`ProviderPool` with asynchronous support for :meth:`~ProviderPool.list_subtitles`.
:param int max_workers: maximum number of threads to use. If `None`, :attr:`max_workers` will be set
to the number of :attr:`~ProviderPool.providers`.
"""
def __init__(self, max_workers=None, *args, **kwargs):
super(AsyncProviderPool, self).__init__(*args, **kwargs)
#: Maximum number of threads to use
self.max_workers = max_workers or len(self.providers)
def list_subtitles_provider(self, provider, video, languages):
return provider, super(AsyncProviderPool, self).list_subtitles_provider(provider, video, languages)
def list_subtitles(self, video, languages):
subtitles = []
with ThreadPoolExecutor(self.max_workers) as executor:
for provider, provider_subtitles in executor.map(self.list_subtitles_provider, self.providers,
itertools.repeat(video, len(self.providers)),
itertools.repeat(languages, len(self.providers))):
# discard provider that failed
if provider_subtitles is None:
logger.info('Discarding provider %s', provider)
self.discarded_providers.add(provider)
continue
# add subtitles
subtitles.extend(provider_subtitles)
return subtitles
def check_video(video, languages=None, age=None, undefined=False):
"""Perform some checks on the `video`.
All the checks are optional. Return `False` if any of this check fails:
* `languages` already exist in `video`'s :attr:`~subliminal.video.Video.subtitle_languages`.
* `video` is older than `age`.
* `video` has an `undefined` language in :attr:`~subliminal.video.Video.subtitle_languages`.
:param video: video to check.
:type video: :class:`~subliminal.video.Video`
:param languages: desired languages.
:type languages: set of :class:`~babelfish.language.Language`
:param datetime.timedelta age: maximum age of the video.
:param bool undefined: fail on existing undefined language.
:return: `True` if the video passes the checks, `False` otherwise.
:rtype: bool
"""
# language test
if languages and not (languages - video.subtitle_languages):
logger.debug('All languages %r exist', languages)
return False
# age test
if age and video.age > age:
logger.debug('Video is older than %r', age)
return False
# undefined test
if undefined and Language('und') in video.subtitle_languages:
logger.debug('Undefined language found')
return False
return True
def search_external_subtitles(path, directory=None):
"""Search for external subtitles from a video `path` and their associated language.
Unless `directory` is provided, search will be made in the same directory as the video file.
:param str path: path to the video.
:param str directory: directory to search for subtitles.
:return: found subtitles with their languages.
:rtype: dict
"""
# split path
dirpath, filename = os.path.split(path)
dirpath = dirpath or '.'
fileroot, fileext = os.path.splitext(filename)
# search for subtitles
subtitles = {}
for p in os.listdir(directory or dirpath):
# keep only valid subtitle filenames
if not p.startswith(fileroot) or not p.endswith(SUBTITLE_EXTENSIONS):
continue
# extract the potential language code
language = Language('und')
language_code = p[len(fileroot):-len(os.path.splitext(p)[1])].replace(fileext, '').replace('_', '-')[1:]
if language_code:
try:
language = Language.fromietf(language_code)
except (ValueError, LanguageReverseError):
logger.error('Cannot parse language code %r', language_code)
subtitles[p] = language
logger.debug('Found subtitles %r', subtitles)
return subtitles
def scan_video(path):
"""Scan a video from a `path`.
:param str path: existing path to the video.
:return: the scanned video.
:rtype: :class:`~subliminal.video.Video`
"""
# check for non-existing path
if not os.path.exists(path):
raise ValueError('Path does not exist')
# check video extension
if not path.endswith(VIDEO_EXTENSIONS):
raise ValueError('%r is not a valid video extension' % os.path.splitext(path)[1])
dirpath, filename = os.path.split(path)
logger.info('Scanning video %r in %r', filename, dirpath)
# guess
video = Video.fromguess(path, guessit(path))
# size and hashes
video.size = os.path.getsize(path)
if video.size > 10485760:
logger.debug('Size is %d', video.size)
video.hashes['opensubtitles'] = hash_opensubtitles(path)
video.hashes['shooter'] = hash_shooter(path)
video.hashes['thesubdb'] = hash_thesubdb(path)
video.hashes['napiprojekt'] = hash_napiprojekt(path)
logger.debug('Computed hashes %r', video.hashes)
else:
logger.warning('Size is lower than 10MB: hashes not computed')
return video
def scan_archive(path):
"""Scan an archive from a `path`.
:param str path: existing path to the archive.
:return: the scanned video.
:rtype: :class:`~subliminal.video.Video`
"""
# check for non-existing path
if not os.path.exists(path):
raise ValueError('Path does not exist')
# check video extension
if not path.endswith(ARCHIVE_EXTENSIONS):
raise ValueError('%r is not a valid archive extension' % os.path.splitext(path)[1])
dirpath, filename = os.path.split(path)
logger.info('Scanning archive %r in %r', filename, dirpath)
# rar extension
if filename.endswith('.rar'):
rar = RarFile(path)
# filter on video extensions
rar_filenames = [f for f in rar.namelist() if f.endswith(VIDEO_EXTENSIONS)]
# no video found
if not rar_filenames:
raise ValueError('No video in archive')
# more than one video found
if len(rar_filenames) > 1:
raise ValueError('More than one video in archive')
# guess
rar_filename = rar_filenames[0]
rar_filepath = os.path.join(dirpath, rar_filename)
video = Video.fromguess(rar_filepath, guessit(rar_filepath))
# size
video.size = rar.getinfo(rar_filename).file_size
else:
raise ValueError('Unsupported extension %r' % os.path.splitext(path)[1])
return video
def scan_videos(path, age=None, archives=True):
"""Scan `path` for videos and their subtitles.
See :func:`refine` to find additional information for the video.
:param str path: existing directory path to scan.
:param datetime.timedelta age: maximum age of the video or archive.
:param bool archives: scan videos in archives.
:return: the scanned videos.
:rtype: list of :class:`~subliminal.video.Video`
"""
# check for non-existing path
if not os.path.exists(path):
raise ValueError('Path does not exist')
# check for non-directory path
if not os.path.isdir(path):
raise ValueError('Path is not a directory')
# walk the path
videos = []
for dirpath, dirnames, filenames in os.walk(path):
logger.debug('Walking directory %r', dirpath)
# remove badly encoded and hidden dirnames
for dirname in list(dirnames):
if dirname.startswith('.'):
logger.debug('Skipping hidden dirname %r in %r', dirname, dirpath)
dirnames.remove(dirname)
# scan for videos
for filename in filenames:
# filter on videos and archives
if not (filename.endswith(VIDEO_EXTENSIONS) or archives and filename.endswith(ARCHIVE_EXTENSIONS)):
continue
# skip hidden files
if filename.startswith('.'):
logger.debug('Skipping hidden filename %r in %r', filename, dirpath)
continue
# reconstruct the file path
filepath = os.path.join(dirpath, filename)
# skip links
if os.path.islink(filepath):
logger.debug('Skipping link %r in %r', filename, dirpath)
continue
# skip old files
if age and datetime.utcnow() - datetime.utcfromtimestamp(os.path.getmtime(filepath)) > age:
logger.debug('Skipping old file %r in %r', filename, dirpath)
continue
# scan
if filename.endswith(VIDEO_EXTENSIONS): # video
try:
video = scan_video(filepath)
except ValueError: # pragma: no cover
logger.exception('Error scanning video')
continue
elif archives and filename.endswith(ARCHIVE_EXTENSIONS): # archive
try:
video = scan_archive(filepath)
except (NotRarFile, RarCannotExec, ValueError): # pragma: no cover
logger.exception('Error scanning archive')
continue
else: # pragma: no cover
raise ValueError('Unsupported file %r' % filename)
videos.append(video)
return videos
def refine(video, episode_refiners=None, movie_refiners=None, **kwargs):
"""Refine a video using :ref:`refiners`.
.. note::
Exceptions raised in refiners are silently passed and logged.
:param video: the video to refine.
:type video: :class:`~subliminal.video.Video`
:param tuple episode_refiners: refiners to use for episodes.
:param tuple movie_refiners: refiners to use for movies.
:param \*\*kwargs: additional parameters for the :func:`~subliminal.refiners.refine` functions.
"""
refiners = ()
if isinstance(video, Episode):
refiners = episode_refiners or ('metadata', 'tvdb', 'omdb')
elif isinstance(video, Movie):
refiners = movie_refiners or ('metadata', 'omdb')
for refiner in refiners:
logger.info('Refining video with %s', refiner)
try:
refiner_manager[refiner].plugin(video, **kwargs)
except:
logger.exception('Failed to refine video')
def list_subtitles(videos, languages, pool_class=ProviderPool, **kwargs):
"""List subtitles.
The `videos` must pass the `languages` check of :func:`check_video`.
:param videos: videos to list subtitles for.
:type videos: set of :class:`~subliminal.video.Video`
:param languages: languages to search for.
:type languages: set of :class:`~babelfish.language.Language`
:param pool_class: class to use as provider pool.
:type pool_class: :class:`ProviderPool`, :class:`AsyncProviderPool` or similar
:param \*\*kwargs: additional parameters for the provided `pool_class` constructor.
:return: found subtitles per video.
:rtype: dict of :class:`~subliminal.video.Video` to list of :class:`~subliminal.subtitle.Subtitle`
"""
listed_subtitles = defaultdict(list)
# check videos
checked_videos = []
for video in videos:
if not check_video(video, languages=languages):
logger.info('Skipping video %r', video)
continue
checked_videos.append(video)
# return immediately if no video passed the checks
if not checked_videos:
return listed_subtitles
# list subtitles
with pool_class(**kwargs) as pool:
for video in checked_videos:
logger.info('Listing subtitles for %r', video)
subtitles = pool.list_subtitles(video, languages - video.subtitle_languages)
listed_subtitles[video].extend(subtitles)
logger.info('Found %d subtitle(s)', len(subtitles))
return listed_subtitles
def download_subtitles(subtitles, pool_class=ProviderPool, **kwargs):
"""Download :attr:`~subliminal.subtitle.Subtitle.content` of `subtitles`.
:param subtitles: subtitles to download.
:type subtitles: list of :class:`~subliminal.subtitle.Subtitle`
:param pool_class: class to use as provider pool.
:type pool_class: :class:`ProviderPool`, :class:`AsyncProviderPool` or similar
:param \*\*kwargs: additional parameters for the provided `pool_class` constructor.
"""
with pool_class(**kwargs) as pool:
for subtitle in subtitles:
logger.info('Downloading subtitle %r', subtitle)
pool.download_subtitle(subtitle)
def download_best_subtitles(videos, languages, min_score=0, hearing_impaired=False, only_one=False, compute_score=None,
pool_class=ProviderPool, **kwargs):
"""List and download the best matching subtitles.
The `videos` must pass the `languages` and `undefined` (`only_one`) checks of :func:`check_video`.
:param videos: videos to download subtitles for.
:type videos: set of :class:`~subliminal.video.Video`
:param languages: languages to download.
:type languages: set of :class:`~babelfish.language.Language`
:param int min_score: minimum score for a subtitle to be downloaded.
:param bool hearing_impaired: hearing impaired preference.
:param bool only_one: download only one subtitle, not one per language.
:param compute_score: function that takes `subtitle` and `video` as positional arguments,
`hearing_impaired` as keyword argument and returns the score.
:param pool_class: class to use as provider pool.
:type pool_class: :class:`ProviderPool`, :class:`AsyncProviderPool` or similar
:param \*\*kwargs: additional parameters for the provided `pool_class` constructor.
:return: downloaded subtitles per video.
:rtype: dict of :class:`~subliminal.video.Video` to list of :class:`~subliminal.subtitle.Subtitle`
"""
downloaded_subtitles = defaultdict(list)
# check videos
checked_videos = []
for video in videos:
if not check_video(video, languages=languages, undefined=only_one):
logger.info('Skipping video %r', video)
continue
checked_videos.append(video)
# return immediately if no video passed the checks
if not checked_videos:
return downloaded_subtitles
# download best subtitles
with pool_class(**kwargs) as pool:
for video in checked_videos:
logger.info('Downloading best subtitles for %r', video)
subtitles = pool.download_best_subtitles(pool.list_subtitles(video, languages - video.subtitle_languages),
video, languages, min_score=min_score,
hearing_impaired=hearing_impaired, only_one=only_one,
compute_score=compute_score)
logger.info('Downloaded %d subtitle(s)', len(subtitles))
downloaded_subtitles[video].extend(subtitles)
return downloaded_subtitles
def save_subtitles(video, subtitles, single=False, directory=None, encoding=None):
"""Save subtitles on filesystem.
Subtitles are saved in the order of the list. If a subtitle with a language has already been saved, other subtitles
with the same language are silently ignored.
The extension used is `.lang.srt` by default or `.srt` is `single` is `True`, with `lang` being the IETF code for
the :attr:`~subliminal.subtitle.Subtitle.language` of the subtitle.
:param video: video of the subtitles.
:type video: :class:`~subliminal.video.Video`
:param subtitles: subtitles to save.
:type subtitles: list of :class:`~subliminal.subtitle.Subtitle`
:param bool single: save a single subtitle, default is to save one subtitle per language.
:param str directory: path to directory where to save the subtitles, default is next to the video.
:param str encoding: encoding in which to save the subtitles, default is to keep original encoding.
:return: the saved subtitles
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
"""
saved_subtitles = []
for subtitle in subtitles:
# check content
if subtitle.content is None:
logger.error('Skipping subtitle %r: no content', subtitle)
continue
# check language
if subtitle.language in set(s.language for s in saved_subtitles):
logger.debug('Skipping subtitle %r: language already saved', subtitle)
continue
# create subtitle path
subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language)
if directory is not None:
subtitle_path = os.path.join(directory, os.path.split(subtitle_path)[1])
# save content as is or in the specified encoding
logger.info('Saving %r to %r', subtitle, subtitle_path)
if encoding is None:
with io.open(subtitle_path, 'wb') as f:
f.write(subtitle.content)
else:
with io.open(subtitle_path, 'w', encoding=encoding) as f:
f.write(subtitle.text)
saved_subtitles.append(subtitle)
# check single
if single:
break
return saved_subtitles
+13 -11
View File
@@ -1,27 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
class Error(Exception):
"""Base class for exceptions in subliminal"""
"""Base class for exceptions in subliminal."""
pass
class ProviderError(Error):
"""Exception raised by providers"""
"""Exception raised by providers."""
pass
class ProviderConfigurationError(ProviderError):
"""Exception raised by providers when badly configured"""
class ConfigurationError(ProviderError):
"""Exception raised by providers when badly configured."""
pass
class ProviderNotAvailable(ProviderError):
"""Exception raised by providers when unavailable"""
class AuthenticationError(ProviderError):
"""Exception raised by providers when authentication failed."""
pass
class InvalidSubtitle(ProviderError):
"""Exception raised by providers when the downloaded subtitle is invalid"""
class TooManyRequests(ProviderError):
"""Exception raised by providers when too many requests are made."""
pass
class DownloadLimitExceeded(ProviderError):
"""Exception raised by providers when download limit is exceeded."""
pass
+106
View File
@@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
from pkg_resources import EntryPoint
from stevedore import ExtensionManager
class RegistrableExtensionManager(ExtensionManager):
""":class:~stevedore.extensions.ExtensionManager` with support for registration.
It allows loading of internal extensions without setup and registering/unregistering additional extensions.
Loading is done in this order:
* Entry point extensions
* Internal extensions
* Registered extensions
:param str namespace: namespace argument for :class:~stevedore.extensions.ExtensionManager`.
:param list internal_extensions: internal extensions to use with entry point syntax.
:param \*\*kwargs: additional parameters for the :class:~stevedore.extensions.ExtensionManager` constructor.
"""
def __init__(self, namespace, internal_extensions, **kwargs):
#: Registered extensions with entry point syntax
self.registered_extensions = []
#: Internal extensions with entry point syntax
self.internal_extensions = internal_extensions
super(RegistrableExtensionManager, self).__init__(namespace, **kwargs)
def _find_entry_points(self, namespace):
# copy of default extensions
eps = list(super(RegistrableExtensionManager, self)._find_entry_points(namespace))
# internal extensions
for iep in self.internal_extensions:
ep = EntryPoint.parse(iep)
if ep.name not in [e.name for e in eps]:
eps.append(ep)
# registered extensions
for rep in self.registered_extensions:
ep = EntryPoint.parse(rep)
if ep.name not in [e.name for e in eps]:
eps.append(ep)
return eps
def register(self, entry_point):
"""Register an extension
:param str entry_point: extension to register (entry point syntax).
:raise: ValueError if already registered.
"""
if entry_point in self.registered_extensions:
raise ValueError('Extension already registered')
ep = EntryPoint.parse(entry_point)
if ep.name in self.names():
raise ValueError('An extension with the same name already exist')
ext = self._load_one_plugin(ep, False, (), {}, False)
self.extensions.append(ext)
if self._extensions_by_name is not None:
self._extensions_by_name[ext.name] = ext
self.registered_extensions.insert(0, entry_point)
def unregister(self, entry_point):
"""Unregister a provider
:param str entry_point: provider to unregister (entry point syntax).
"""
if entry_point not in self.registered_extensions:
raise ValueError('Extension not registered')
ep = EntryPoint.parse(entry_point)
self.registered_extensions.remove(entry_point)
if self._extensions_by_name is not None:
del self._extensions_by_name[ep.name]
for i, ext in enumerate(self.extensions):
if ext.name == ep.name:
del self.extensions[i]
break
#: Provider manager
provider_manager = RegistrableExtensionManager('subliminal.providers', [
'addic7ed = subliminal.providers.addic7ed:Addic7edProvider',
'legendastv = subliminal.providers.legendastv:LegendasTVProvider',
'opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider',
'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider',
'shooter = subliminal.providers.shooter:ShooterProvider',
'subscenter = subliminal.providers.subscenter:SubsCenterProvider',
'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider',
'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider'
])
#: Refiner manager
refiner_manager = RegistrableExtensionManager('subliminal.refiners', [
'metadata = subliminal.refiners.metadata:refine',
'omdb = subliminal.refiners.omdb:refine',
'tvdb = subliminal.refiners.tvdb:refine'
])
+84 -54
View File
@@ -1,19 +1,65 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import babelfish
import logging
from bs4 import BeautifulSoup, FeatureNotFound
from six.moves.xmlrpc_client import SafeTransport
from ..video import Episode, Movie
logger = logging.getLogger(__name__)
class TimeoutSafeTransport(SafeTransport):
"""Timeout support for ``xmlrpc.client.SafeTransport``."""
def __init__(self, timeout, *args, **kwargs):
SafeTransport.__init__(self, *args, **kwargs)
self.timeout = timeout
def make_connection(self, host):
c = SafeTransport.make_connection(self, host)
c.timeout = self.timeout
return c
class ParserBeautifulSoup(BeautifulSoup):
"""A ``bs4.BeautifulSoup`` that picks the first parser available in `parsers`.
:param markup: markup for the ``bs4.BeautifulSoup``.
:param list parsers: parser names, in order of preference.
"""
def __init__(self, markup, parsers, **kwargs):
# reject features
if set(parsers).intersection({'fast', 'permissive', 'strict', 'xml', 'html', 'html5'}):
raise ValueError('Features not allowed, only parser names')
# reject some kwargs
if 'features' in kwargs:
raise ValueError('Cannot use features kwarg')
if 'builder' in kwargs:
raise ValueError('Cannot use builder kwarg')
# pick the first parser available
for parser in parsers:
try:
super(ParserBeautifulSoup, self).__init__(markup, parser, **kwargs)
return
except FeatureNotFound:
pass
raise FeatureNotFound
class Provider(object):
"""Base class for providers
"""Base class for providers.
If any configuration is possible for the provider, like credentials, it must take place during instantiation
If any configuration is possible for the provider, like credentials, it must take place during instantiation.
:param \*\*kwargs: configuration
:raise: :class:`~subliminal.exceptions.ProviderConfigurationError` if there is a configuration error
:raise: :class:`~subliminal.exceptions.ConfigurationError` if there is a configuration error
"""
#: Supported BabelFish languages
#: Supported set of :class:`~babelfish.language.Language`
languages = set()
#: Supported video types
@@ -22,53 +68,46 @@ class Provider(object):
#: Required hash, if any
required_hash = None
def __init__(self, **kwargs):
pass
def __enter__(self):
self.initialize()
return self
def __exit__(self, *args):
def __exit__(self, exc_type, exc_value, traceback):
self.terminate()
def initialize(self):
"""Initialize the provider
"""Initialize the provider.
Must be called when starting to work with the provider. This is the place for network initialization
or login operations.
.. note:
This is called automatically if you use the :keyword:`with` statement
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
.. note::
This is called automatically when entering the `with` statement
"""
pass
raise NotImplementedError
def terminate(self):
"""Terminate the provider
"""Terminate the provider.
Must be called when done with the provider. This is the place for network shutdown or logout operations.
.. note:
This is called automatically if you use the :keyword:`with` statement
.. note::
This is called automatically when exiting the `with` statement
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
"""
pass
raise NotImplementedError
@classmethod
def check(cls, video):
"""Check if the `video` can be processed
"""Check if the `video` can be processed.
The video is considered invalid if not an instance of :attr:`video_types` or if the :attr:`required_hash` is
not present in :attr:`~subliminal.video.Video`'s `hashes` attribute.
The `video` is considered invalid if not an instance of :attr:`video_types` or if the :attr:`required_hash` is
not present in :attr:`~subliminal.video.Video.hashes` attribute of the `video`.
:param video: the video to check
:param video: the video to check.
:type video: :class:`~subliminal.video.Video`
:return: `True` if the `video` and `languages` are valid, `False` otherwise
:return: `True` if the `video` is valid, `False` otherwise.
:rtype: bool
"""
@@ -76,53 +115,44 @@ class Provider(object):
return False
if cls.required_hash is not None and cls.required_hash not in video.hashes:
return False
return True
def query(self, languages, *args, **kwargs):
"""Query the provider for subtitles
def query(self, *args, **kwargs):
"""Query the provider for subtitles.
This method arguments match as much as possible the actual parameters for querying the provider
Arguments should match as much as possible the actual parameters for querying the provider
:param languages: languages to search for
:type languages: set of :class:`babelfish.Language`
:param \*args: other required arguments
:param \*\*kwargs: other optional arguments
:return: the subtitles
:return: found subtitles.
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
:raise: :class:`~subliminal.exceptions.ProviderError` if something unexpected occured
:raise: :class:`~subliminal.exceptions.ProviderError`
"""
raise NotImplementedError
def list_subtitles(self, video, languages):
"""List subtitles for the `video` with the given `languages`
"""List subtitles for the `video` with the given `languages`.
This is a proxy for the :meth:`query` method. The parameters passed to the :meth:`query` method may
vary depending on the amount of information available in the `video`
This will call the :meth:`query` method internally. The parameters passed to the :meth:`query` method may
vary depending on the amount of information available in the `video`.
:param video: video to list subtitles for
:param video: video to list subtitles for.
:type video: :class:`~subliminal.video.Video`
:param languages: languages to search for
:type languages: set of :class:`babelfish.Language`
:return: the subtitles
:param languages: languages to search for.
:type languages: set of :class:`~babelfish.language.Language`
:return: found subtitles.
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
:raise: :class:`~subliminal.exceptions.ProviderError` if something unexpected occured
:raise: :class:`~subliminal.exceptions.ProviderError`
"""
raise NotImplementedError
def download_subtitle(self, subtitle):
"""Download the `subtitle`
"""Download `subtitle`'s :attr:`~subliminal.subtitle.Subtitle.content`.
:param subtitle: subtitle to download
:param subtitle: subtitle to download.
:type subtitle: :class:`~subliminal.subtitle.Subtitle`
:return: the subtitle text
:rtype: string
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
:raise: :class:`~subliminal.exceptions.InvalidSubtitle` if the downloaded subtitle is invalid
:raise: :class:`~subliminal.exceptions.ProviderError` if something unexpected occured
:raise: :class:`~subliminal.exceptions.ProviderError`
"""
raise NotImplementedError
+209 -115
View File
@@ -1,38 +1,51 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import babelfish
import bs4
import charade
import requests
from . import Provider
from .. import __version__
from ..cache import region
from ..exceptions import ProviderConfigurationError, ProviderNotAvailable, InvalidSubtitle
from ..subtitle import Subtitle, is_valid_subtitle
from ..video import Episode
import re
from babelfish import Language, language_converters
from guessit import guessit
from requests import Session
from . import ParserBeautifulSoup, Provider
from .. import __short_version__
from ..cache import SHOW_EXPIRATION_TIME, region
from ..exceptions import AuthenticationError, ConfigurationError, DownloadLimitExceeded, TooManyRequests
from ..subtitle import Subtitle, fix_line_ending, guess_matches
from ..utils import sanitize, sanitize_release_group
from ..video import Episode
logger = logging.getLogger(__name__)
language_converters.register('addic7ed = subliminal.converters.addic7ed:Addic7edConverter')
#: Series header parsing regex
series_year_re = re.compile(r'^(?P<series>[ \w\'.:-]+)(?: \((?P<year>\d{4})\))?$')
class Addic7edSubtitle(Subtitle):
"""Addic7ed Subtitle."""
provider_name = 'addic7ed'
def __init__(self, language, series, season, episode, title, version, hearing_impaired, download_link, referer):
super(Addic7edSubtitle, self).__init__(language, hearing_impaired)
def __init__(self, language, hearing_impaired, page_link, series, season, episode, title, year, version,
download_link):
super(Addic7edSubtitle, self).__init__(language, hearing_impaired, page_link)
self.series = series
self.season = season
self.episode = episode
self.title = title
self.year = year
self.version = version
self.download_link = download_link
self.referer = referer
def compute_matches(self, video):
@property
def id(self):
return self.download_link
def get_matches(self, video):
matches = set()
# series
if video.series and self.series == video.series:
if video.series and sanitize(self.series) == sanitize(video.series):
matches.add('series')
# season
if video.season and self.season == video.season:
@@ -41,151 +54,232 @@ class Addic7edSubtitle(Subtitle):
if video.episode and self.episode == video.episode:
matches.add('episode')
# title
if video.title and self.title.lower() == video.title.lower():
if video.title and sanitize(self.title) == sanitize(video.title):
matches.add('title')
# year
if video.original_series and self.year is None or video.year and video.year == self.year:
matches.add('year')
# release_group
if video.release_group and self.version and video.release_group.lower() in self.version.lower():
if (video.release_group and self.version and
sanitize_release_group(video.release_group) in sanitize_release_group(self.version)):
matches.add('release_group')
# resolution
if video.resolution and self.version and video.resolution in self.version.lower():
matches.add('resolution')
# format
if video.format and self.version and video.format.lower() in self.version.lower():
matches.add('format')
# other properties
matches |= guess_matches(video, guessit(self.version), partial=True)
return matches
class Addic7edProvider(Provider):
languages = {babelfish.Language('por', 'BR')} | {babelfish.Language(l)
for l in ['ara', 'aze', 'ben', 'bos', 'bul', 'cat', 'ces', 'dan', 'deu', 'ell', 'eng', 'eus', 'fas',
'fin', 'fra', 'glg', 'heb', 'hrv', 'hun', 'hye', 'ind', 'ita', 'jpn', 'kor', 'mkd', 'msa',
'nld', 'nor', 'pol', 'por', 'ron', 'rus', 'slk', 'slv', 'spa', 'sqi', 'srp', 'swe', 'tha',
'tur', 'ukr', 'vie', 'zho']}
"""Addic7ed Provider."""
languages = {Language('por', 'BR')} | {Language(l) for l in [
'ara', 'aze', 'ben', 'bos', 'bul', 'cat', 'ces', 'dan', 'deu', 'ell', 'eng', 'eus', 'fas', 'fin', 'fra', 'glg',
'heb', 'hrv', 'hun', 'hye', 'ind', 'ita', 'jpn', 'kor', 'mkd', 'msa', 'nld', 'nor', 'pol', 'por', 'ron', 'rus',
'slk', 'slv', 'spa', 'sqi', 'srp', 'swe', 'tha', 'tur', 'ukr', 'vie', 'zho'
]}
video_types = (Episode,)
server = 'http://www.addic7ed.com'
server_url = 'http://www.addic7ed.com/'
def __init__(self, username=None, password=None):
if username is not None and password is None or username is None and password is not None:
raise ProviderConfigurationError('Username and password must be specified')
raise ConfigurationError('Username and password must be specified')
self.username = username
self.password = password
self.logged_in = False
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
self.session = Session()
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
# login
if self.username is not None and self.password is not None:
logger.debug('Logging in')
logger.info('Logging in')
data = {'username': self.username, 'password': self.password, 'Submit': 'Log in'}
try:
r = self.session.post(self.server + '/dologin.php', data, timeout=10, allow_redirects=False)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code == 302:
logger.info('Logged in')
self.logged_in = True
else:
logger.error('Failed to login')
r = self.session.post(self.server_url + 'dologin.php', data, allow_redirects=False, timeout=10)
if r.status_code != 302:
raise AuthenticationError(self.username)
logger.debug('Logged in')
self.logged_in = True
def terminate(self):
# logout
if self.logged_in:
try:
r = self.session.get(self.server + '/logout.php', timeout=10)
logger.info('Logged out')
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
logger.info('Logging out')
r = self.session.get(self.server_url + 'logout.php', timeout=10)
r.raise_for_status()
logger.debug('Logged out')
self.logged_in = False
self.session.close()
def get(self, url, params=None):
"""Make a GET request on `url` with the given parameters
@region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME)
def _get_show_ids(self):
"""Get the ``dict`` of show ids per series by querying the `shows.php` page.
:param string url: part of the URL to reach with the leading slash
:param params: params of the request
:return: the response
:rtype: :class:`bs4.BeautifulSoup`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
try:
r = self.session.get(self.server + url, params=params, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
return bs4.BeautifulSoup(r.content, ['permissive'])
@region.cache_on_arguments()
def get_show_ids(self):
"""Load the shows page with default series to show ids mapping
:return: series to show ids
:return: show id per series, lower case and without quotes.
:rtype: dict
"""
soup = self.get('/shows.php')
# get the show page
logger.info('Getting show ids')
r = self.session.get(self.server_url + 'shows.php', timeout=10)
r.raise_for_status()
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
# populate the show ids
show_ids = {}
for html_show in soup.select('td.version > h3 > a[href^="/show/"]'):
show_ids[html_show.string.lower()] = int(html_show['href'][6:])
for show in soup.select('td.version > h3 > a[href^="/show/"]'):
show_ids[sanitize(show.text)] = int(show['href'][6:])
logger.debug('Found %d show ids', len(show_ids))
return show_ids
@region.cache_on_arguments()
def find_show_id(self, series):
"""Find a show id from the series
@region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME)
def _search_show_id(self, series, year=None):
"""Search the show id from the `series` and `year`.
Use this only if the series is not in the dict returned by :meth:`get_show_ids`
:param string series: series of the episode
:return: the show id, if any
:rtype: int or None
:param str series: series of the episode.
:param year: year of the series, if any.
:type year: int
:return: the show id, if found.
:rtype: int
"""
params = {'search': series, 'Submit': 'Search'}
logger.debug('Searching series %r', params)
suggested_shows = self.get('/search.php', params).select('span.titulo > a[href^="/show/"]')
if not suggested_shows:
logger.info('Series %r not found', series)
return None
return int(suggested_shows[0]['href'][6:])
# addic7ed doesn't support search with quotes
series = series.replace('\'', ' ')
def query(self, series, season):
show_ids = self.get_show_ids()
if series.lower() in show_ids:
show_id = show_ids[series.lower()]
else:
show_id = self.find_show_id(series.lower())
if show_id is None:
return []
params = {'show_id': show_id, 'season': season}
logger.debug('Searching subtitles %r', params)
link = '/show/{show_id}&season={season}'.format(**params)
soup = self.get(link)
# build the params
series_year = '%s %d' % (series, year) if year is not None else series
params = {'search': series_year, 'Submit': 'Search'}
# make the search
logger.info('Searching show ids with %r', params)
r = self.session.get(self.server_url + 'search.php', params=params, timeout=10)
r.raise_for_status()
if r.status_code == 304:
raise TooManyRequests()
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
# get the suggestion
suggestion = soup.select('span.titulo > a[href^="/show/"]')
if not suggestion:
logger.warning('Show id not found: no suggestion')
return None
if not sanitize(suggestion[0].i.text.replace('\'', ' ')) == sanitize(series_year):
logger.warning('Show id not found: suggestion does not match')
return None
show_id = int(suggestion[0]['href'][6:])
logger.debug('Found show id %d', show_id)
return show_id
def get_show_id(self, series, year=None, country_code=None):
"""Get the best matching show id for `series`, `year` and `country_code`.
First search in the result of :meth:`_get_show_ids` and fallback on a search with :meth:`_search_show_id`.
:param str series: series of the episode.
:param year: year of the series, if any.
:type year: int
:param country_code: country code of the series, if any.
:type country_code: str
:return: the show id, if found.
:rtype: int
"""
series_sanitized = sanitize(series).lower()
show_ids = self._get_show_ids()
show_id = None
# attempt with country
if not show_id and country_code:
logger.debug('Getting show id with country')
show_id = show_ids.get('%s %s' % (series_sanitized, country_code.lower()))
# attempt with year
if not show_id and year:
logger.debug('Getting show id with year')
show_id = show_ids.get('%s %d' % (series_sanitized, year))
# attempt clean
if not show_id:
logger.debug('Getting show id')
show_id = show_ids.get(series_sanitized)
# search as last resort
if not show_id:
logger.warning('Series not found in show ids')
show_id = self._search_show_id(series)
return show_id
def query(self, series, season, year=None, country=None):
# get the show id
show_id = self.get_show_id(series, year, country)
if show_id is None:
logger.error('No show id found for %r (%r)', series, {'year': year, 'country': country})
return []
# get the page of the season of the show
logger.info('Getting the page of show id %d, season %d', show_id, season)
r = self.session.get(self.server_url + 'show/%d' % show_id, params={'season': season}, timeout=10)
r.raise_for_status()
if r.status_code == 304:
raise TooManyRequests()
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
# loop over subtitle rows
match = series_year_re.match(soup.select('#header font')[0].text.strip()[:-10])
series = match.group('series')
year = int(match.group('year')) if match.group('year') else None
subtitles = []
for row in soup('tr', class_='epeven completed'):
for row in soup.select('tr.epeven'):
cells = row('td')
if cells[5].string != 'Completed':
logger.debug('Skipping incomplete subtitle')
# ignore incomplete subtitles
status = cells[5].text
if status != 'Completed':
logger.debug('Ignoring subtitle with status %s', status)
continue
subtitles.append(Addic7edSubtitle(babelfish.Language.fromaddic7ed(cells[3].string), series, season,
int(cells[1].string), cells[2].string, cells[4].string,
bool(cells[6].string), cells[9].a['href'], link))
# read the item
language = Language.fromaddic7ed(cells[3].text)
hearing_impaired = bool(cells[6].text)
page_link = self.server_url + cells[2].a['href'][1:]
season = int(cells[0].text)
episode = int(cells[1].text)
title = cells[2].text
version = cells[4].text
download_link = cells[9].a['href'][1:]
subtitle = Addic7edSubtitle(language, hearing_impaired, page_link, series, season, episode, title, year,
version, download_link)
logger.debug('Found subtitle %r', subtitle)
subtitles.append(subtitle)
return subtitles
def list_subtitles(self, video, languages):
return [s for s in self.query(video.series, video.season)
return [s for s in self.query(video.series, video.season, video.year)
if s.language in languages and s.episode == video.episode]
def download_subtitle(self, subtitle):
try:
r = self.session.get(self.server + subtitle.download_link, timeout=10,
headers={'Referer': self.server + subtitle.referer})
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
# download the subtitle
logger.info('Downloading subtitle %r', subtitle)
r = self.session.get(self.server_url + subtitle.download_link, headers={'Referer': subtitle.page_link},
timeout=10)
r.raise_for_status()
# detect download limit exceeded
if r.headers['Content-Type'] == 'text/html':
raise ProviderNotAvailable('Download limit exceeded')
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
raise DownloadLimitExceeded
subtitle.content = fix_line_ending(r.content)
-135
View File
@@ -1,135 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import urllib
import babelfish
import charade
import guessit
import requests
import xml.etree.ElementTree
from . import Provider
from .. import __version__
from ..cache import region
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
from ..subtitle import Subtitle, is_valid_subtitle, compute_guess_matches
from ..video import Episode
logger = logging.getLogger(__name__)
class BierDopjeSubtitle(Subtitle):
provider_name = 'bierdopje'
def __init__(self, language, season, episode, tvdb_id, series, filename, download_link):
super(BierDopjeSubtitle, self).__init__(language)
self.season = season
self.episode = episode
self.tvdb_id = tvdb_id
self.series = series
self.filename = filename
self.download_link = download_link
def compute_matches(self, video):
matches = set()
# tvdb_id
if video.tvdb_id and self.tvdb_id == video.tvdb_id:
matches.add('tvdb_id')
# series
if video.series and self.series == video.series:
matches.add('series')
# season
if video.season and self.season == video.season:
matches.add('season')
# episode
if video.episode and self.episode == video.episode:
matches.add('episode')
matches |= compute_guess_matches(video, guessit.guess_episode_info(self.filename + '.mkv'))
return matches
class BierDopjeProvider(Provider):
languages = {babelfish.Language(l) for l in ['eng', 'nld']}
video_types = (Episode,)
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
def terminate(self):
self.session.close()
def get(self, url, **params):
"""Make a GET request on the `url` formatted with `**params`
:param string url: API part of the URL to reach without the leading slash
:param \*\*params: format specs for the `url`
:return: the response
:rtype: :class:`xml.etree.ElementTree.Element`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
try:
r = self.session.get('http://api.bierdopje.com/A2B638AC5D804C2E/' + url.format(**params), timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code == 429:
raise ProviderNotAvailable('Too Many Requests')
elif r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
return xml.etree.ElementTree.fromstring(r.content)
@region.cache_on_arguments()
def find_show_id(self, series):
"""Find the show id from series name
:param string series: series of the episode
:return: show id
:rtype: int
"""
logger.debug('Searching for series %r', series)
root = self.get('FindShowByName/{series}', series=urllib.quote(series))
if root.find('response/status').text == 'false':
logger.info('Series %r not found', series)
return None
return int(root.find('response/results/result[1]/showid').text)
def query(self, language, season, episode, tvdb_id=None, series=None):
params = {'language': language.alpha2, 'season': season, 'episode': episode}
if tvdb_id is not None:
params['showid'] = tvdb_id
params['istvdbid'] = 'true'
elif series is not None:
show_id = self.find_show_id(series)
if show_id is None:
return []
params['showid'] = show_id
params['istvdbid'] = 'false'
else:
raise ValueError('Missing parameter tvdb_id or series')
logger.debug('Searching subtitles %r', params)
root = self.get('GetAllSubsFor/{showid}/{season}/{episode}/{language}/{istvdbid}', **params)
if root.find('response/status').text == 'false':
logger.debug('No subtitle found')
return []
logger.debug('Found subtitles %r', root.find('response/results'))
return [BierDopjeSubtitle(language, season, episode, tvdb_id, series, result.find('filename').text,
result.find('downloadlink').text) for result in root.find('response/results')]
def list_subtitles(self, video, languages):
return [s for l in languages for s in self.query(l, video.season, video.episode, video.tvdb_id, video.series)]
def download_subtitle(self, subtitle):
try:
r = self.session.get(subtitle.download_link, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code == 429:
raise ProviderNotAvailable('Too Many Requests')
elif r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+448
View File
@@ -0,0 +1,448 @@
# -*- coding: utf-8 -*-
import io
import json
import logging
import os
import re
from babelfish import Language, language_converters
from datetime import datetime, timedelta
from dogpile.cache.api import NO_VALUE
from guessit import guessit
import pytz
import rarfile
from rarfile import RarFile, is_rarfile
from requests import Session
from zipfile import ZipFile, is_zipfile
from . import ParserBeautifulSoup, Provider
from .. import __short_version__
from ..cache import SHOW_EXPIRATION_TIME, region
from ..exceptions import AuthenticationError, ConfigurationError, ProviderError
from ..subtitle import SUBTITLE_EXTENSIONS, Subtitle, fix_line_ending, guess_matches, sanitize
from ..video import Episode, Movie
logger = logging.getLogger(__name__)
language_converters.register('legendastv = subliminal.converters.legendastv:LegendasTVConverter')
# Configure :mod:`rarfile` to use the same path separator as :mod:`zipfile`
rarfile.PATH_SEP = '/'
#: Conversion map for types
type_map = {'M': 'movie', 'S': 'episode', 'C': 'episode'}
#: BR title season parsing regex
season_re = re.compile(r' - (?P<season>\d+)(\xaa|a|st|nd|rd|th) (temporada|season)', re.IGNORECASE)
#: Downloads parsing regex
downloads_re = re.compile(r'(?P<downloads>\d+) downloads')
#: Rating parsing regex
rating_re = re.compile(r'nota (?P<rating>\d+)')
#: Timestamp parsing regex
timestamp_re = re.compile(r'(?P<day>\d+)/(?P<month>\d+)/(?P<year>\d+) - (?P<hour>\d+):(?P<minute>\d+)')
#: Cache key for releases
releases_key = __name__ + ':releases|{archive_id}'
class LegendasTVArchive(object):
"""LegendasTV Archive.
:param str id: identifier.
:param str name: name.
:param bool pack: contains subtitles for multiple episodes.
:param bool pack: featured.
:param str link: link.
:param int downloads: download count.
:param int rating: rating (0-10).
:param timestamp: timestamp.
:type timestamp: datetime.datetime
"""
def __init__(self, id, name, pack, featured, link, downloads=0, rating=0, timestamp=None):
#: Identifier
self.id = id
#: Name
self.name = name
#: Pack
self.pack = pack
#: Featured
self.featured = featured
#: Link
self.link = link
#: Download count
self.downloads = downloads
#: Rating (0-10)
self.rating = rating
#: Timestamp
self.timestamp = timestamp
#: Compressed content as :class:`rarfile.RarFile` or :class:`zipfile.ZipFile`
self.content = None
def __repr__(self):
return '<%s [%s] %r>' % (self.__class__.__name__, self.id, self.name)
class LegendasTVSubtitle(Subtitle):
"""LegendasTV Subtitle."""
provider_name = 'legendastv'
def __init__(self, language, type, title, year, imdb_id, season, archive, name):
super(LegendasTVSubtitle, self).__init__(language, archive.link)
self.type = type
self.title = title
self.year = year
self.imdb_id = imdb_id
self.season = season
self.archive = archive
self.name = name
@property
def id(self):
return '%s-%s' % (self.archive.id, self.name.lower())
def get_matches(self, video, hearing_impaired=False):
matches = set()
# episode
if isinstance(video, Episode) and self.type == 'episode':
# series
if video.series and sanitize(self.title) == sanitize(video.series):
matches.add('series')
# year (year is based on season air date hence the adjustment)
if video.original_series and self.year is None or video.year and video.year == self.year - self.season + 1:
matches.add('year')
# imdb_id
if video.series_imdb_id and self.imdb_id == video.series_imdb_id:
matches.add('series_imdb_id')
# movie
elif isinstance(video, Movie) and self.type == 'movie':
# title
if video.title and sanitize(self.title) == sanitize(video.title):
matches.add('title')
# year
if video.year and self.year == video.year:
matches.add('year')
# imdb_id
if video.imdb_id and self.imdb_id == video.imdb_id:
matches.add('imdb_id')
# archive name
matches |= guess_matches(video, guessit(self.archive.name, {'type': self.type}))
# name
matches |= guess_matches(video, guessit(self.name, {'type': self.type}))
return matches
class LegendasTVProvider(Provider):
"""LegendasTV Provider.
:param str username: username.
:param str password: password.
"""
languages = {Language.fromlegendastv(l) for l in language_converters['legendastv'].codes}
server_url = 'http://legendas.tv/'
def __init__(self, username=None, password=None):
if username and not password or not username and password:
raise ConfigurationError('Username and password must be specified')
self.username = username
self.password = password
self.logged_in = False
def initialize(self):
self.session = Session()
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
# login
if self.username is not None and self.password is not None:
logger.info('Logging in')
data = {'_method': 'POST', 'data[User][username]': self.username, 'data[User][password]': self.password}
r = self.session.post(self.server_url + 'login', data, allow_redirects=False, timeout=10)
r.raise_for_status()
soup = ParserBeautifulSoup(r.content, ['html.parser'])
if soup.find('div', {'class': 'alert-error'}, string=re.compile(u'Usuário ou senha inválidos')):
raise AuthenticationError(self.username)
logger.debug('Logged in')
self.logged_in = True
def terminate(self):
# logout
if self.logged_in:
logger.info('Logging out')
r = self.session.get(self.server_url + 'users/logout', allow_redirects=False, timeout=10)
r.raise_for_status()
logger.debug('Logged out')
self.logged_in = False
self.session.close()
@region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME)
def search_titles(self, title):
"""Search for titles matching the `title`.
:param str title: the title to search for.
:return: found titles.
:rtype: dict
"""
# make the query
logger.info('Searching title %r', title)
r = self.session.get(self.server_url + 'legenda/sugestao/{}'.format(title), timeout=10)
r.raise_for_status()
results = json.loads(r.text)
# loop over results
titles = {}
for result in results:
source = result['_source']
# extract id
title_id = int(source['id_filme'])
# extract type and title
title = {'type': type_map[source['tipo']], 'title': source['dsc_nome']}
# extract year
if source['dsc_data_lancamento'] and source['dsc_data_lancamento'].isdigit():
title['year'] = int(source['dsc_data_lancamento'])
# extract imdb_id
if source['id_imdb'] != '0':
if not source['id_imdb'].startswith('tt'):
title['imdb_id'] = 'tt' + source['id_imdb'].zfill(7)
else:
title['imdb_id'] = source['id_imdb']
# extract season
if title['type'] == 'episode':
if source['temporada'] and source['temporada'].isdigit():
title['season'] = int(source['temporada'])
else:
match = season_re.search(source['dsc_nome_br'])
if match:
title['season'] = int(match.group('season'))
else:
logger.warning('No season detected for title %d', title_id)
# add title
titles[title_id] = title
logger.debug('Found %d titles', len(titles))
return titles
@region.cache_on_arguments(expiration_time=timedelta(minutes=15).total_seconds())
def get_archives(self, title_id, language_code):
"""Get the archive list from a given `title_id` and `language_code`.
:param int title_id: title id.
:param int language_code: language code.
:return: the archives.
:rtype: list of :class:`LegendasTVArchive`
"""
logger.info('Getting archives for title %d and language %d', title_id, language_code)
archives = []
page = 1
while True:
# get the archive page
url = self.server_url + 'util/carrega_legendas_busca_filme/{title}/{language}/-/{page}'.format(
title=title_id, language=language_code, page=page)
r = self.session.get(url)
r.raise_for_status()
# parse the results
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
for archive_soup in soup.select('div.list_element > article > div'):
# create archive
archive = LegendasTVArchive(archive_soup.a['href'].split('/')[2], archive_soup.a.text,
'pack' in archive_soup['class'], 'destaque' in archive_soup['class'],
self.server_url + archive_soup.a['href'][1:])
# extract text containing downloads, rating and timestamp
data_text = archive_soup.find('p', class_='data').text
# match downloads
archive.downloads = int(downloads_re.search(data_text).group('downloads'))
# match rating
match = rating_re.search(data_text)
if match:
archive.rating = int(match.group('rating'))
# match timestamp and validate it
time_data = {k: int(v) for k, v in timestamp_re.search(data_text).groupdict().items()}
archive.timestamp = pytz.timezone('America/Sao_Paulo').localize(datetime(**time_data))
if archive.timestamp > datetime.utcnow().replace(tzinfo=pytz.utc):
raise ProviderError('Archive timestamp is in the future')
# add archive
archives.append(archive)
# stop on last page
if soup.find('a', attrs={'class': 'load_more'}, string='carregar mais') is None:
break
# increment page count
page += 1
logger.debug('Found %d archives', len(archives))
return archives
def download_archive(self, archive):
"""Download an archive's :attr:`~LegendasTVArchive.content`.
:param archive: the archive to download :attr:`~LegendasTVArchive.content` of.
:type archive: :class:`LegendasTVArchive`
"""
logger.info('Downloading archive %s', archive.id)
r = self.session.get(self.server_url + 'downloadarquivo/{}'.format(archive.id))
r.raise_for_status()
# open the archive
archive_stream = io.BytesIO(r.content)
if is_rarfile(archive_stream):
logger.debug('Identified rar archive')
archive.content = RarFile(archive_stream)
elif is_zipfile(archive_stream):
logger.debug('Identified zip archive')
archive.content = ZipFile(archive_stream)
else:
raise ValueError('Not a valid archive')
def query(self, language, title, season=None, episode=None, year=None):
# search for titles
titles = self.search_titles(sanitize(title))
# search for titles with the quote or dot character
ignore_characters = {'\'', '.'}
if any(c in title for c in ignore_characters):
titles.update(self.search_titles(sanitize(title, ignore_characters=ignore_characters)))
subtitles = []
# iterate over titles
for title_id, t in titles.items():
# discard mismatches on title
if sanitize(t['title']) != sanitize(title):
continue
# episode
if season and episode:
# discard mismatches on type
if t['type'] != 'episode':
continue
# discard mismatches on season
if 'season' not in t or t['season'] != season:
continue
# movie
else:
# discard mismatches on type
if t['type'] != 'movie':
continue
# discard mismatches on year
if year is not None and 'year' in t and t['year'] != year:
continue
# iterate over title's archives
for a in self.get_archives(title_id, language.legendastv):
# clean name of path separators and pack flags
clean_name = a.name.replace('/', '-')
if a.pack and clean_name.startswith('(p)'):
clean_name = clean_name[3:]
# guess from name
guess = guessit(clean_name, {'type': t['type']})
# episode
if season and episode:
# discard mismatches on episode in non-pack archives
if not a.pack and 'episode' in guess and guess['episode'] != episode:
continue
# compute an expiration time based on the archive timestamp
expiration_time = (datetime.utcnow().replace(tzinfo=pytz.utc) - a.timestamp).total_seconds()
# attempt to get the releases from the cache
releases = region.get(releases_key.format(archive_id=a.id), expiration_time=expiration_time)
# the releases are not in cache or cache is expired
if releases == NO_VALUE:
logger.info('Releases not found in cache')
# download archive
self.download_archive(a)
# extract the releases
releases = []
for name in a.content.namelist():
# discard the legendastv file
if name.startswith('Legendas.tv'):
continue
# discard hidden files
if os.path.split(name)[-1].startswith('.'):
continue
# discard non-subtitle files
if not name.lower().endswith(SUBTITLE_EXTENSIONS):
continue
releases.append(name)
# cache the releases
region.set(releases_key.format(archive_id=a.id), releases)
# iterate over releases
for r in releases:
subtitle = LegendasTVSubtitle(language, t['type'], t['title'], t.get('year'), t.get('imdb_id'),
t.get('season'), a, r)
logger.debug('Found subtitle %r', subtitle)
subtitles.append(subtitle)
return subtitles
def list_subtitles(self, video, languages):
season = episode = None
if isinstance(video, Episode):
title = video.series
season = video.season
episode = video.episode
else:
title = video.title
return [s for l in languages for s in self.query(l, title, season=season, episode=episode, year=video.year)]
def download_subtitle(self, subtitle):
# download archive in case we previously hit the releases cache and didn't download it
if subtitle.archive.content is None:
self.download_archive(subtitle.archive)
# extract subtitle's content
subtitle.content = fix_line_ending(subtitle.archive.content.read(subtitle.name))
+103
View File
@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
import logging
from babelfish import Language
from requests import Session
from . import Provider
from .. import __short_version__
from ..subtitle import Subtitle
logger = logging.getLogger(__name__)
def get_subhash(hash):
"""Get a second hash based on napiprojekt's hash.
:param str hash: napiprojekt's hash.
:return: the subhash.
:rtype: str
"""
idx = [0xe, 0x3, 0x6, 0x8, 0x2]
mul = [2, 2, 5, 4, 3]
add = [0, 0xd, 0x10, 0xb, 0x5]
b = []
for i in range(len(idx)):
a = add[i]
m = mul[i]
i = idx[i]
t = a + int(hash[i], 16)
v = int(hash[t:t + 2], 16)
b.append(('%x' % (v * m))[-1])
return ''.join(b)
class NapiProjektSubtitle(Subtitle):
"""NapiProjekt Subtitle."""
provider_name = 'napiprojekt'
def __init__(self, language, hash):
super(NapiProjektSubtitle, self).__init__(language)
self.hash = hash
@property
def id(self):
return self.hash
def get_matches(self, video):
matches = set()
# hash
if 'napiprojekt' in video.hashes and video.hashes['napiprojekt'] == self.hash:
matches.add('hash')
return matches
class NapiProjektProvider(Provider):
"""NapiProjekt Provider."""
languages = {Language.fromalpha2(l) for l in ['pl']}
required_hash = 'napiprojekt'
server_url = 'http://napiprojekt.pl/unit_napisy/dl.php'
def initialize(self):
self.session = Session()
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
def terminate(self):
self.session.close()
def query(self, language, hash):
params = {
'v': 'dreambox',
'kolejka': 'false',
'nick': '',
'pass': '',
'napios': 'Linux',
'l': language.alpha2.upper(),
'f': hash,
't': get_subhash(hash)}
logger.info('Searching subtitle %r', params)
response = self.session.get(self.server_url, params=params, timeout=10)
response.raise_for_status()
# handle subtitles not found and errors
if response.content[:4] == b'NPc0':
logger.debug('No subtitles found')
return None
subtitle = NapiProjektSubtitle(language, hash)
subtitle.content = response.content
logger.debug('Found subtitle %r', subtitle)
return subtitle
def list_subtitles(self, video, languages):
return [s for s in [self.query(l, video.hashes['napiprojekt']) for l in languages] if s is not None]
def download_subtitle(self, subtitle):
# there is no download step, content is already filled from listing subtitles
pass
+219 -84
View File
@@ -1,32 +1,33 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import base64
import logging
import os
import re
import xmlrpclib
import zlib
import babelfish
import charade
import guessit
from . import Provider
from .. import __version__
from ..exceptions import ProviderError, ProviderNotAvailable, InvalidSubtitle
from ..subtitle import Subtitle, is_valid_subtitle, compute_guess_matches
from ..video import Episode, Movie
from babelfish import Language, language_converters
from guessit import guessit
from six.moves.xmlrpc_client import ServerProxy
from . import Provider, TimeoutSafeTransport
from .. import __short_version__
from ..exceptions import AuthenticationError, ConfigurationError, DownloadLimitExceeded, ProviderError
from ..subtitle import Subtitle, fix_line_ending, guess_matches
from ..utils import sanitize
from ..video import Episode, Movie
logger = logging.getLogger(__name__)
class OpenSubtitlesSubtitle(Subtitle):
"""OpenSubtitles Subtitle."""
provider_name = 'opensubtitles'
series_re = re.compile('^"(?P<series_name>.*)" (?P<series_title>.*)$')
series_re = re.compile(r'^"(?P<series_name>.*)" (?P<series_title>.*)$')
def __init__(self, language, hearing_impaired, id, matched_by, movie_kind, hash, movie_name, movie_release_name, movie_year,
movie_imdb_id, series_season, series_episode):
super(OpenSubtitlesSubtitle, self).__init__(language, hearing_impaired)
self.id = id
def __init__(self, language, hearing_impaired, page_link, subtitle_id, matched_by, movie_kind, hash, movie_name,
movie_release_name, movie_year, movie_imdb_id, series_season, series_episode, filename, encoding):
super(OpenSubtitlesSubtitle, self).__init__(language, hearing_impaired, page_link, encoding)
self.subtitle_id = subtitle_id
self.matched_by = matched_by
self.movie_kind = movie_kind
self.hash = hash
@@ -36,6 +37,11 @@ class OpenSubtitlesSubtitle(Subtitle):
self.movie_imdb_id = movie_imdb_id
self.series_season = series_season
self.series_episode = series_episode
self.filename = filename
@property
def id(self):
return str(self.subtitle_id)
@property
def series_name(self):
@@ -45,115 +51,244 @@ class OpenSubtitlesSubtitle(Subtitle):
def series_title(self):
return self.series_re.match(self.movie_name).group('series_title')
def compute_matches(self, video):
def get_matches(self, video):
matches = set()
# episode
if isinstance(video, Episode) and self.movie_kind == 'episode':
# tag match, assume series, year, season and episode matches
if self.matched_by == 'tag':
matches |= {'series', 'year', 'season', 'episode'}
# series
if video.series and self.series_name.lower() == video.series.lower():
if video.series and sanitize(self.series_name) == sanitize(video.series):
matches.add('series')
# year
if video.original_series and self.movie_year is None or video.year and video.year == self.movie_year:
matches.add('year')
# season
if video.season and self.series_season == video.season:
matches.add('season')
# episode
if video.episode and self.series_episode == video.episode:
matches.add('episode')
# title
if video.title and sanitize(self.series_title) == sanitize(video.title):
matches.add('title')
# guess
matches |= compute_guess_matches(video, guessit.guess_episode_info(self.movie_release_name + '.mkv'))
matches |= guess_matches(video, guessit(self.movie_release_name, {'type': 'episode'}))
matches |= guess_matches(video, guessit(self.filename, {'type': 'episode'}))
# hash
if 'opensubtitles' in video.hashes and self.hash == video.hashes['opensubtitles']:
if 'series' in matches and 'season' in matches and 'episode' in matches:
matches.add('hash')
else:
logger.debug('Match on hash discarded')
# movie
elif isinstance(video, Movie) and self.movie_kind == 'movie':
# tag match, assume title and year matches
if self.matched_by == 'tag':
matches |= {'title', 'year'}
# title
if video.title and sanitize(self.movie_name) == sanitize(video.title):
matches.add('title')
# year
if video.year and self.movie_year == video.year:
matches.add('year')
# guess
matches |= compute_guess_matches(video, guessit.guess_movie_info(self.movie_release_name + '.mkv'))
matches |= guess_matches(video, guessit(self.movie_release_name, {'type': 'movie'}))
matches |= guess_matches(video, guessit(self.filename, {'type': 'movie'}))
# hash
if 'opensubtitles' in video.hashes and self.hash == video.hashes['opensubtitles']:
if 'title' in matches:
matches.add('hash')
else:
logger.debug('Match on hash discarded')
else:
logger.info('%r is not a valid movie_kind for %r', self.movie_kind, video)
logger.info('%r is not a valid movie_kind', self.movie_kind)
return matches
# hash
if 'opensubtitles' in video.hashes and self.hash == video.hashes['opensubtitles']:
matches.add('hash')
# imdb_id
if video.imdb_id and self.movie_imdb_id == video.imdb_id:
matches.add('imdb_id')
# title
if video.title and self.movie_name.lower() == video.title.lower():
matches.add('title')
return matches
class OpenSubtitlesProvider(Provider):
languages = {babelfish.Language.fromopensubtitles(l) for l in babelfish.CONVERTERS['opensubtitles'].codes}
"""OpenSubtitles Provider.
def __init__(self):
self.server = xmlrpclib.ServerProxy('http://api.opensubtitles.org/xml-rpc')
:param str username: username.
:param str password: password.
"""
languages = {Language.fromopensubtitles(l) for l in language_converters['opensubtitles'].codes}
def __init__(self, username=None, password=None):
self.server = ServerProxy('https://api.opensubtitles.org/xml-rpc', TimeoutSafeTransport(10))
if username and not password or not username and password:
raise ConfigurationError('Username and password must be specified')
# None values not allowed for logging in, so replace it by ''
self.username = username or ''
self.password = password or ''
self.token = None
def initialize(self):
try:
response = self.server.LogIn('', '', 'eng', 'subliminal v%s' % __version__)
except xmlrpclib.ProtocolError:
raise ProviderNotAvailable
if response['status'] != '200 OK':
raise ProviderError('Login failed with status %r' % response['status'])
logger.info('Logging in')
response = checked(self.server.LogIn(self.username, self.password, 'eng',
'subliminal v%s' % __short_version__))
self.token = response['token']
logger.debug('Logged in with token %r', self.token)
def terminate(self):
try:
response = self.server.LogOut(self.token)
except xmlrpclib.ProtocolError:
raise ProviderNotAvailable
if response['status'] != '200 OK':
raise ProviderError('Logout failed with status %r' % response['status'])
logger.info('Logging out')
checked(self.server.LogOut(self.token))
self.server.close()
self.token = None
logger.debug('Logged out')
def query(self, languages, hash=None, size=None, imdb_id=None, query=None):
searches = []
def no_operation(self):
logger.debug('No operation')
checked(self.server.NoOperation(self.token))
def query(self, languages, hash=None, size=None, imdb_id=None, query=None, season=None, episode=None, tag=None):
# fill the search criteria
criteria = []
if hash and size:
searches.append({'moviehash': hash, 'moviebytesize': str(size)})
criteria.append({'moviehash': hash, 'moviebytesize': str(size)})
if imdb_id:
searches.append({'imdbid': imdb_id})
if query:
searches.append({'query': query})
if not searches:
raise ValueError('One or more parameter missing')
for search in searches:
search['sublanguageid'] = ','.join(l.opensubtitles for l in languages)
logger.debug('Searching subtitles %r', searches)
try:
response = self.server.SearchSubtitles(self.token, searches)
except xmlrpclib.ProtocolError:
raise ProviderNotAvailable
if response['status'] != '200 OK':
raise ProviderError('Search failed with status %r' % response['status'])
criteria.append({'imdbid': imdb_id[2:]})
if tag:
criteria.append({'tag': tag})
if query and season and episode:
criteria.append({'query': query.replace('\'', ''), 'season': season, 'episode': episode})
elif query:
criteria.append({'query': query.replace('\'', '')})
if not criteria:
raise ValueError('Not enough information')
# add the language
for criterion in criteria:
criterion['sublanguageid'] = ','.join(sorted(l.opensubtitles for l in languages))
# query the server
logger.info('Searching subtitles %r', criteria)
response = checked(self.server.SearchSubtitles(self.token, criteria))
subtitles = []
# exit if no data
if not response['data']:
logger.debug('No subtitle found')
return []
return [OpenSubtitlesSubtitle(babelfish.Language.fromopensubtitles(r['SubLanguageID']),
bool(int(r['SubHearingImpaired'])), r['IDSubtitleFile'], r['MatchedBy'],
r['MovieKind'], r['MovieHash'], r['MovieName'], r['MovieReleaseName'],
int(r['MovieYear']) if r['MovieYear'] else None, int(r['IDMovieImdb']),
int(r['SeriesSeason']) if r['SeriesSeason'] else None,
int(r['SeriesEpisode']) if r['SeriesEpisode'] else None)
for r in response['data']]
logger.debug('No subtitles found')
return subtitles
# loop over subtitle items
for subtitle_item in response['data']:
# read the item
language = Language.fromopensubtitles(subtitle_item['SubLanguageID'])
hearing_impaired = bool(int(subtitle_item['SubHearingImpaired']))
page_link = subtitle_item['SubtitlesLink']
subtitle_id = int(subtitle_item['IDSubtitleFile'])
matched_by = subtitle_item['MatchedBy']
movie_kind = subtitle_item['MovieKind']
hash = subtitle_item['MovieHash']
movie_name = subtitle_item['MovieName']
movie_release_name = subtitle_item['MovieReleaseName']
movie_year = int(subtitle_item['MovieYear']) if subtitle_item['MovieYear'] else None
movie_imdb_id = 'tt' + subtitle_item['IDMovieImdb']
series_season = int(subtitle_item['SeriesSeason']) if subtitle_item['SeriesSeason'] else None
series_episode = int(subtitle_item['SeriesEpisode']) if subtitle_item['SeriesEpisode'] else None
filename = subtitle_item['SubFileName']
encoding = subtitle_item.get('SubEncoding') or None
subtitle = OpenSubtitlesSubtitle(language, hearing_impaired, page_link, subtitle_id, matched_by, movie_kind,
hash, movie_name, movie_release_name, movie_year, movie_imdb_id,
series_season, series_episode, filename, encoding)
logger.debug('Found subtitle %r by %s', subtitle, matched_by)
subtitles.append(subtitle)
return subtitles
def list_subtitles(self, video, languages):
query = None
if ('opensubtitles' not in video.hashes or not video.size) and not video.imdb_id:
query = video.name.split(os.sep)[-1]
season = episode = None
if isinstance(video, Episode):
query = video.series
season = video.season
episode = video.episode
else:
query = video.title
return self.query(languages, hash=video.hashes.get('opensubtitles'), size=video.size, imdb_id=video.imdb_id,
query=query)
query=query, season=season, episode=episode, tag=os.path.basename(video.name))
def download_subtitle(self, subtitle):
try:
response = self.server.DownloadSubtitles(self.token, [subtitle.id])
except xmlrpclib.ProtocolError:
raise ProviderNotAvailable
if response['status'] != '200 OK':
raise ProviderError('Download failed with status %r' % response['status'])
if not response['data']:
raise ProviderError('Nothing to download')
subtitle_bytes = zlib.decompress(base64.b64decode(response['data'][0]['data']), 47)
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
logger.info('Downloading subtitle %r', subtitle)
response = checked(self.server.DownloadSubtitles(self.token, [str(subtitle.subtitle_id)]))
subtitle.content = fix_line_ending(zlib.decompress(base64.b64decode(response['data'][0]['data']), 47))
class OpenSubtitlesError(ProviderError):
"""Base class for non-generic :class:`OpenSubtitlesProvider` exceptions."""
pass
class Unauthorized(OpenSubtitlesError, AuthenticationError):
"""Exception raised when status is '401 Unauthorized'."""
pass
class NoSession(OpenSubtitlesError, AuthenticationError):
"""Exception raised when status is '406 No session'."""
pass
class DownloadLimitReached(OpenSubtitlesError, DownloadLimitExceeded):
"""Exception raised when status is '407 Download limit reached'."""
pass
class InvalidImdbid(OpenSubtitlesError):
"""Exception raised when status is '413 Invalid ImdbID'."""
pass
class UnknownUserAgent(OpenSubtitlesError, AuthenticationError):
"""Exception raised when status is '414 Unknown User Agent'."""
pass
class DisabledUserAgent(OpenSubtitlesError, AuthenticationError):
"""Exception raised when status is '415 Disabled user agent'."""
pass
class ServiceUnavailable(OpenSubtitlesError):
"""Exception raised when status is '503 Service Unavailable'."""
pass
def checked(response):
"""Check a response status before returning it.
:param response: a response from a XMLRPC call to OpenSubtitles.
:return: the response.
:raise: :class:`OpenSubtitlesError`
"""
status_code = int(response['status'][:3])
if status_code == 401:
raise Unauthorized
if status_code == 406:
raise NoSession
if status_code == 407:
raise DownloadLimitReached
if status_code == 413:
raise InvalidImdbid
if status_code == 414:
raise UnknownUserAgent
if status_code == 415:
raise DisabledUserAgent
if status_code == 503:
raise ServiceUnavailable
if status_code != 200:
raise OpenSubtitlesError(response['status'])
return response
+115 -99
View File
@@ -1,48 +1,59 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import io
import logging
import re
import xml.etree.ElementTree
import zipfile
import babelfish
import bs4
import charade
import guessit
import requests
from . import Provider
from .. import __version__
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
from ..subtitle import Subtitle, is_valid_subtitle, compute_guess_matches
from ..video import Episode, Movie
from babelfish import Language, language_converters
from guessit import guessit
try:
from lxml import etree
except ImportError:
try:
import xml.etree.cElementTree as etree
except ImportError:
import xml.etree.ElementTree as etree
from requests import Session
from zipfile import ZipFile
from . import Provider
from .. import __short_version__
from ..exceptions import ProviderError
from ..subtitle import Subtitle, fix_line_ending, guess_matches
from ..utils import sanitize
from ..video import Episode, Movie
logger = logging.getLogger(__name__)
class PodnapisiSubtitle(Subtitle):
"""Podnapisi Subtitle."""
provider_name = 'podnapisi'
def __init__(self, language, id, releases, hearing_impaired, link, series=None, season=None, episode=None,
title=None, year=None):
super(PodnapisiSubtitle, self).__init__(language, hearing_impaired)
self.id = id
def __init__(self, language, hearing_impaired, page_link, pid, releases, title, season=None, episode=None,
year=None):
super(PodnapisiSubtitle, self).__init__(language, hearing_impaired, page_link)
self.pid = pid
self.releases = releases
self.hearing_impaired = hearing_impaired
self.link = link
self.series = series
self.title = title
self.season = season
self.episode = episode
self.title = title
self.year = year
def compute_matches(self, video):
@property
def id(self):
return self.pid
def get_matches(self, video):
matches = set()
# episode
if isinstance(video, Episode):
# series
if video.series and self.series.lower() == video.series.lower():
if video.series and sanitize(self.title) == sanitize(video.series):
matches.add('series')
# year
if video.original_series and self.year is None or video.year and video.year == self.year:
matches.add('year')
# season
if video.season and self.season == video.season:
matches.add('season')
@@ -51,113 +62,118 @@ class PodnapisiSubtitle(Subtitle):
matches.add('episode')
# guess
for release in self.releases:
matches |= compute_guess_matches(video, guessit.guess_episode_info(release + '.mkv'))
matches |= guess_matches(video, guessit(release, {'type': 'episode'}))
# movie
elif isinstance(video, Movie):
# title
if video.title and self.title.lower() == video.title.lower():
if video.title and sanitize(self.title) == sanitize(video.title):
matches.add('title')
# year
if video.year and self.year == video.year:
matches.add('year')
# guess
for release in self.releases:
matches |= compute_guess_matches(video, guessit.guess_movie_info(release + '.mkv'))
matches |= guess_matches(video, guessit(release, {'type': 'movie'}))
return matches
class PodnapisiProvider(Provider):
languages = {babelfish.Language.frompodnapisi(l) for l in babelfish.CONVERTERS['podnapisi'].codes}
video_types = (Episode, Movie)
server = 'http://simple.podnapisi.net'
link_re = re.compile('^.*(?P<link>/ppodnapisi/download/i/\d+/k/.*$)')
"""Podnapisi Provider."""
languages = ({Language('por', 'BR'), Language('srp', script='Latn')} |
{Language.fromalpha2(l) for l in language_converters['alpha2'].codes})
server_url = 'http://podnapisi.net/subtitles/'
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
self.session = Session()
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
def terminate(self):
self.session.close()
def get(self, url, params=None, is_xml=True):
"""Make a GET request on `url` with the given parameters
:param string url: part of the URL to reach with the leading slash
:param dict params: params of the request
:param bool xml: whether the response content is XML or not
:return: the response
:rtype: :class:`xml.etree.ElementTree.Element` or :class:`bs4.BeautifulSoup`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
try:
r = self.session.get(self.server + '/ppodnapisi' + url, params=params, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
if is_xml:
return xml.etree.ElementTree.fromstring(r.content)
else:
return bs4.BeautifulSoup(r.content, ['permissive'])
def query(self, language, series=None, season=None, episode=None, title=None, year=None):
params = {'sXML': 1, 'sJ': language.podnapisi}
if series and season and episode:
params['sK'] = series
def query(self, language, keyword, season=None, episode=None, year=None):
# set parameters, see http://www.podnapisi.net/forum/viewtopic.php?f=62&t=26164#p212652
params = {'sXML': 1, 'sL': str(language), 'sK': keyword}
is_episode = False
if season and episode:
is_episode = True
params['sTS'] = season
params['sTE'] = episode
elif title:
params['sK'] = title
if year:
params['sY'] = year
else:
raise ValueError('Missing parameters series and season and episode or title')
logger.debug('Searching episode %r', params)
if year:
params['sY'] = year
# loop over paginated results
logger.info('Searching subtitles %r', params)
subtitles = []
pids = set()
while True:
root = self.get('/search', params)
if not int(root.find('pagination/results').text):
logger.debug('No subtitle found')
# query the server
xml = etree.fromstring(self.session.get(self.server_url + 'search/old', params=params, timeout=10).content)
# exit if no results
if not int(xml.find('pagination/results').text):
logger.debug('No subtitles found')
break
if series and season and episode:
subtitles.extend([PodnapisiSubtitle(language, int(s.find('id').text), s.find('release').text.split(),
'h' in (s.find('flags').text or ''), s.find('url').text[38:],
series=series, season=season, episode=episode)
for s in root.findall('subtitle')])
elif title:
subtitles.extend([PodnapisiSubtitle(language, int(s.find('id').text), s.find('release').text.split(),
'h' in (s.find('flags').text or ''), s.find('url').text[38:],
title=title, year=year)
for s in root.findall('subtitle')])
if int(root.find('pagination/current').text) >= int(root.find('pagination/count').text):
# loop over subtitles
for subtitle_xml in xml.findall('subtitle'):
# read xml elements
language = Language.fromietf(subtitle_xml.find('language').text)
hearing_impaired = 'n' in (subtitle_xml.find('flags').text or '')
page_link = subtitle_xml.find('url').text
pid = subtitle_xml.find('pid').text
releases = []
if subtitle_xml.find('release').text:
for release in subtitle_xml.find('release').text.split():
release = re.sub(r'\.+$', '', release) # remove trailing dots
release = ''.join(filter(lambda x: ord(x) < 128, release)) # remove non-ascii characters
releases.append(release)
title = subtitle_xml.find('title').text
season = int(subtitle_xml.find('tvSeason').text)
episode = int(subtitle_xml.find('tvEpisode').text)
year = int(subtitle_xml.find('year').text)
if is_episode:
subtitle = PodnapisiSubtitle(language, hearing_impaired, page_link, pid, releases, title,
season=season, episode=episode, year=year)
else:
subtitle = PodnapisiSubtitle(language, hearing_impaired, page_link, pid, releases, title,
year=year)
# ignore duplicates, see http://www.podnapisi.net/forum/viewtopic.php?f=62&t=26164&start=10#p213321
if pid in pids:
continue
logger.debug('Found subtitle %r', subtitle)
subtitles.append(subtitle)
pids.add(pid)
# stop on last page
if int(xml.find('pagination/current').text) >= int(xml.find('pagination/count').text):
break
params['page'] = int(root.find('pagination/current').text) + 1
# increment current page
params['page'] = int(xml.find('pagination/current').text) + 1
logger.debug('Getting page %d', params['page'])
return subtitles
def list_subtitles(self, video, languages):
if isinstance(video, Episode):
return [s for l in languages for s in self.query(l, series=video.series, season=video.season,
episode=video.episode)]
return [s for l in languages for s in self.query(l, video.series, season=video.season,
episode=video.episode, year=video.year)]
elif isinstance(video, Movie):
return [s for l in languages for s in self.query(l, title=video.title, year=video.year)]
return [s for l in languages for s in self.query(l, video.title, year=video.year)]
def download_subtitle(self, subtitle):
soup = self.get(subtitle.link, is_xml=False)
link = soup.find('a', href=self.link_re)
if not link:
raise ProviderError('Cannot find the download link')
try:
r = self.session.get(self.server + self.link_re.match(link['href']).group('link'), timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
with zipfile.ZipFile(io.BytesIO(r.content)) as zf:
# download as a zip
logger.info('Downloading subtitle %r', subtitle)
r = self.session.get(self.server_url + subtitle.pid + '/download', params={'container': 'zip'}, timeout=10)
r.raise_for_status()
# open the zip
with ZipFile(io.BytesIO(r.content)) as zf:
if len(zf.namelist()) > 1:
raise ProviderError('More than one file to unzip')
subtitle_bytes = zf.read(zf.namelist()[0])
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
subtitle.content = fix_line_ending(zf.read(zf.namelist()[0]))
+79
View File
@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
import json
import logging
import os
from babelfish import Language, language_converters
from requests import Session
from . import Provider
from .. import __short_version__
from ..subtitle import Subtitle, fix_line_ending
logger = logging.getLogger(__name__)
language_converters.register('shooter = subliminal.converters.shooter:ShooterConverter')
class ShooterSubtitle(Subtitle):
"""Shooter Subtitle."""
provider_name = 'shooter'
def __init__(self, language, hash, download_link):
super(ShooterSubtitle, self).__init__(language)
self.hash = hash
self.download_link = download_link
@property
def id(self):
return self.download_link
def get_matches(self, video):
matches = set()
# hash
if 'shooter' in video.hashes and video.hashes['shooter'] == self.hash:
matches.add('hash')
return matches
class ShooterProvider(Provider):
"""Shooter Provider."""
languages = {Language(l) for l in ['eng', 'zho']}
server_url = 'https://www.shooter.cn/api/subapi.php'
def initialize(self):
self.session = Session()
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
def terminate(self):
self.session.close()
def query(self, language, filename, hash=None):
# query the server
params = {'filehash': hash, 'pathinfo': os.path.realpath(filename), 'format': 'json', 'lang': language.shooter}
logger.debug('Searching subtitles %r', params)
r = self.session.post(self.server_url, params=params, timeout=10)
r.raise_for_status()
# handle subtitles not found
if r.content == b'\xff':
logger.debug('No subtitles found')
return []
# parse the subtitles
results = json.loads(r.text)
subtitles = [ShooterSubtitle(language, hash, t['Link']) for s in results for t in s['Files']]
return subtitles
def list_subtitles(self, video, languages):
return [s for l in languages for s in self.query(l, video.name, video.hashes.get('shooter'))]
def download_subtitle(self, subtitle):
logger.info('Downloading subtitle %r', subtitle)
r = self.session.get(subtitle.download_link, timeout=10)
r.raise_for_status()
subtitle.content = fix_line_ending(r.content)
+228
View File
@@ -0,0 +1,228 @@
# -*- coding: utf-8 -*-
import bisect
from collections import defaultdict
import io
import json
import logging
import zipfile
from babelfish import Language
from guessit import guessit
from requests import Session
from . import ParserBeautifulSoup, Provider
from .. import __short_version__
from ..cache import SHOW_EXPIRATION_TIME, region
from ..exceptions import AuthenticationError, ConfigurationError, ProviderError
from ..subtitle import Subtitle, fix_line_ending, guess_matches
from ..utils import sanitize
from ..video import Episode, Movie
logger = logging.getLogger(__name__)
class SubsCenterSubtitle(Subtitle):
"""SubsCenter Subtitle."""
provider_name = 'subscenter'
def __init__(self, language, hearing_impaired, page_link, series, season, episode, title, subtitle_id, subtitle_key,
downloaded, releases):
super(SubsCenterSubtitle, self).__init__(language, hearing_impaired, page_link)
self.series = series
self.season = season
self.episode = episode
self.title = title
self.subtitle_id = subtitle_id
self.subtitle_key = subtitle_key
self.downloaded = downloaded
self.releases = releases
@property
def id(self):
return str(self.subtitle_id)
def get_matches(self, video):
matches = set()
# episode
if isinstance(video, Episode):
# series
if video.series and sanitize(self.series) == sanitize(video.series):
matches.add('series')
# season
if video.season and self.season == video.season:
matches.add('season')
# episode
if video.episode and self.episode == video.episode:
matches.add('episode')
# guess
for release in self.releases:
matches |= guess_matches(video, guessit(release, {'type': 'episode'}))
# movie
elif isinstance(video, Movie):
# guess
for release in self.releases:
matches |= guess_matches(video, guessit(release, {'type': 'movie'}))
# title
if video.title and sanitize(self.title) == sanitize(video.title):
matches.add('title')
return matches
class SubsCenterProvider(Provider):
"""SubsCenter Provider."""
languages = {Language.fromalpha2(l) for l in ['he']}
server_url = 'http://subscenter.cinemast.com/he/'
def __init__(self, username=None, password=None):
if username is not None and password is None or username is None and password is not None:
raise ConfigurationError('Username and password must be specified')
self.username = username
self.password = password
self.logged_in = False
def initialize(self):
self.session = Session()
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
# login
if self.username is not None and self.password is not None:
logger.debug('Logging in')
url = self.server_url + 'subscenter/accounts/login/'
# retrieve CSRF token
self.session.get(url)
csrf_token = self.session.cookies['csrftoken']
# actual login
data = {'username': self.username, 'password': self.password, 'csrfmiddlewaretoken': csrf_token}
r = self.session.post(url, data, allow_redirects=False, timeout=10)
if r.status_code != 302:
raise AuthenticationError(self.username)
logger.info('Logged in')
self.logged_in = True
def terminate(self):
# logout
if self.logged_in:
logger.info('Logging out')
r = self.session.get(self.server_url + 'subscenter/accounts/logout/', timeout=10)
r.raise_for_status()
logger.info('Logged out')
self.logged_in = False
self.session.close()
@region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME)
def _search_url_titles(self, title):
"""Search the URL titles by kind for the given `title`.
:param str title: title to search for.
:return: the URL titles by kind.
:rtype: collections.defaultdict
"""
# make the search
logger.info('Searching title name for %r', title)
r = self.session.get(self.server_url + 'subtitle/search/', params={'q': title}, timeout=10)
r.raise_for_status()
# get the suggestions
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
links = soup.select('#processes div.generalWindowTop a')
logger.debug('Found %d suggestions', len(links))
url_titles = defaultdict(list)
for link in links:
parts = link.attrs['href'].split('/')
url_titles[parts[-3]].append(parts[-2])
return url_titles
def query(self, title, season=None, episode=None):
# search for the url title
url_titles = self._search_url_titles(title)
# episode
if season and episode:
if 'series' not in url_titles:
logger.error('No URL title found for series %r', title)
return []
url_title = url_titles['series'][0]
logger.debug('Using series title %r', url_title)
url = self.server_url + 'cinemast/data/series/sb/{}/{}/{}/'.format(url_title, season, episode)
page_link = self.server_url + 'subtitle/series/{}/{}/{}/'.format(url_title, season, episode)
else:
if 'movie' not in url_titles:
logger.error('No URL title found for movie %r', title)
return []
url_title = url_titles['movie'][0]
logger.debug('Using movie title %r', url_title)
url = self.server_url + 'cinemast/data/movie/sb/{}/'.format(url_title)
page_link = self.server_url + 'subtitle/movie/{}/'.format(url_title)
# get the list of subtitles
logger.debug('Getting the list of subtitles')
r = self.session.get(url)
r.raise_for_status()
results = json.loads(r.text)
# loop over results
subtitles = {}
for language_code, language_data in results.items():
for quality_data in language_data.values():
for quality, subtitles_data in quality_data.items():
for subtitle_item in subtitles_data.values():
# read the item
language = Language.fromalpha2(language_code)
hearing_impaired = bool(subtitle_item['hearing_impaired'])
subtitle_id = subtitle_item['id']
subtitle_key = subtitle_item['key']
downloaded = subtitle_item['downloaded']
release = subtitle_item['subtitle_version']
# add the release and increment downloaded count if we already have the subtitle
if subtitle_id in subtitles:
logger.debug('Found additional release %r for subtitle %d', release, subtitle_id)
bisect.insort_left(subtitles[subtitle_id].releases, release) # deterministic order
subtitles[subtitle_id].downloaded += downloaded
continue
# otherwise create it
subtitle = SubsCenterSubtitle(language, hearing_impaired, page_link, title, season, episode,
title, subtitle_id, subtitle_key, downloaded, [release])
logger.debug('Found subtitle %r', subtitle)
subtitles[subtitle_id] = subtitle
return subtitles.values()
def list_subtitles(self, video, languages):
season = episode = None
title = video.title
if isinstance(video, Episode):
title = video.series
season = video.season
episode = video.episode
return [s for s in self.query(title, season, episode) if s.language in languages]
def download_subtitle(self, subtitle):
# download
url = self.server_url + 'subtitle/download/{}/{}/'.format(subtitle.language.alpha2, subtitle.subtitle_id)
params = {'v': subtitle.releases[0], 'key': subtitle.subtitle_key}
r = self.session.get(url, params=params, headers={'Referer': subtitle.page_link}, timeout=10)
r.raise_for_status()
# open the zip
with zipfile.ZipFile(io.BytesIO(r.content)) as zf:
# remove some filenames from the namelist
namelist = [n for n in zf.namelist() if not n.endswith('.txt')]
if len(namelist) > 1:
raise ProviderError('More than one file to unzip')
subtitle.content = fix_line_ending(zf.read(namelist[0]))
+45 -42
View File
@@ -1,81 +1,84 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import babelfish
import charade
import requests
from . import Provider
from .. import __version__
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
from ..subtitle import Subtitle, is_valid_subtitle
from babelfish import Language, language_converters
from requests import Session
from . import Provider
from .. import __short_version__
from ..subtitle import Subtitle, fix_line_ending
logger = logging.getLogger(__name__)
language_converters.register('thesubdb = subliminal.converters.thesubdb:TheSubDBConverter')
class TheSubDBSubtitle(Subtitle):
"""TheSubDB Subtitle."""
provider_name = 'thesubdb'
def __init__(self, language, hash):
super(TheSubDBSubtitle, self).__init__(language)
self.hash = hash
def compute_matches(self, video):
@property
def id(self):
return self.hash + '-' + str(self.language)
def get_matches(self, video):
matches = set()
# hash
if 'thesubdb' in video.hashes and video.hashes['thesubdb'] == self.hash:
matches.add('hash')
return matches
class TheSubDBProvider(Provider):
languages = {babelfish.Language.fromalpha2(l) for l in ['en', 'es', 'fr', 'it', 'nl', 'pl', 'pt', 'ro', 'sv', 'tr']}
"""TheSubDB Provider."""
languages = {Language.fromthesubdb(l) for l in language_converters['thesubdb'].codes}
required_hash = 'thesubdb'
server_url = 'http://api.thesubdb.com/'
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'SubDB/1.0 (subliminal/%s; https://github.com/Diaoul/subliminal)' %
__version__}
self.session = Session()
self.session.headers['User-Agent'] = ('SubDB/1.0 (subliminal/%s; https://github.com/Diaoul/subliminal)' %
__short_version__)
def terminate(self):
self.session.close()
def get(self, params):
"""Make a GET request on the server with the given parameters
:param params: params of the request
:return: the response
:rtype: :class:`requests.Response`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
try:
r = self.session.get('http://api.thesubdb.com', params=params, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
return r
def query(self, hash):
# make the query
params = {'action': 'search', 'hash': hash}
logger.debug('Searching subtitles %r', params)
r = self.get(params)
logger.info('Searching subtitles %r', params)
r = self.session.get(self.server_url, params=params, timeout=10)
# handle subtitles not found and errors
if r.status_code == 404:
logger.debug('No subtitle found')
logger.debug('No subtitles found')
return []
elif r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
return [TheSubDBSubtitle(language, hash) for language in
{babelfish.Language.fromalpha2(l) for l in r.content.split(',')}]
r.raise_for_status()
# loop over languages
subtitles = []
for language_code in r.text.split(','):
language = Language.fromthesubdb(language_code)
subtitle = TheSubDBSubtitle(language, hash)
logger.debug('Found subtitle %r', subtitle)
subtitles.append(subtitle)
return subtitles
def list_subtitles(self, video, languages):
return [s for s in self.query(video.hashes['thesubdb']) if s.language in languages]
def download_subtitle(self, subtitle):
logger.info('Downloading subtitle %r', subtitle)
params = {'action': 'download', 'hash': subtitle.hash, 'language': subtitle.language.alpha2}
r = self.get(params)
if r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
r = self.session.get(self.server_url, params=params, timeout=10)
r.raise_for_status()
subtitle.content = fix_line_ending(r.content)
+141 -108
View File
@@ -1,40 +1,52 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import io
import logging
import re
import zipfile
import babelfish
import bs4
import charade
import requests
from . import Provider
from .. import __version__
from ..cache import region
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
from ..subtitle import Subtitle, is_valid_subtitle
from ..video import Episode
from zipfile import ZipFile
from babelfish import Language, language_converters
from guessit import guessit
from requests import Session
from . import ParserBeautifulSoup, Provider
from .. import __short_version__
from ..cache import EPISODE_EXPIRATION_TIME, SHOW_EXPIRATION_TIME, region
from ..exceptions import ProviderError
from ..subtitle import Subtitle, fix_line_ending, guess_matches
from ..utils import sanitize, sanitize_release_group
from ..video import Episode
logger = logging.getLogger(__name__)
language_converters.register('tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter')
link_re = re.compile(r'^(?P<series>.+?)(?: \(?\d{4}\)?| \((?:US|UK)\))? \((?P<first_year>\d{4})-\d{4}\)$')
episode_id_re = re.compile(r'^episode-\d+\.html$')
class TVsubtitlesSubtitle(Subtitle):
"""TVsubtitles Subtitle."""
provider_name = 'tvsubtitles'
def __init__(self, language, series, season, episode, id, rip, release):
super(TVsubtitlesSubtitle, self).__init__(language)
def __init__(self, language, page_link, subtitle_id, series, season, episode, year, rip, release):
super(TVsubtitlesSubtitle, self).__init__(language, page_link=page_link)
self.subtitle_id = subtitle_id
self.series = series
self.season = season
self.episode = episode
self.id = id
self.year = year
self.rip = rip
self.release = release
def compute_matches(self, video):
@property
def id(self):
return str(self.subtitle_id)
def get_matches(self, video):
matches = set()
# series
if video.series and self.series == video.series:
if video.series and sanitize(self.series) == sanitize(video.series):
matches.add('series')
# season
if video.season and self.season == video.season:
@@ -42,134 +54,155 @@ class TVsubtitlesSubtitle(Subtitle):
# episode
if video.episode and self.episode == video.episode:
matches.add('episode')
# year
if video.original_series and self.year is None or video.year and video.year == self.year:
matches.add('year')
# release_group
if video.release_group and self.release and video.release_group.lower() in self.release.lower():
if (video.release_group and self.release and
sanitize_release_group(video.release_group) in sanitize_release_group(self.release)):
matches.add('release_group')
# video_codec
if video.video_codec and self.release and (video.video_codec in self.release.lower()
or video.video_codec == 'h264' and 'x264' in self.release.lower()):
matches.add('video_codec')
# resolution
if video.resolution and self.rip and video.resolution in self.rip.lower():
matches.add('resolution')
# other properties
if self.release:
matches |= guess_matches(video, guessit(self.release, {'type': 'episode'}), partial=True)
if self.rip:
matches |= guess_matches(video, guessit(self.rip), partial=True)
return matches
class TVsubtitlesProvider(Provider):
languages = {babelfish.Language('por', 'BR')} | {babelfish.Language(l)
for l in ['ara', 'bul', 'ces', 'dan', 'deu', 'ell', 'eng', 'fin', 'fra', 'hun', 'ita', 'jpn', 'kor',
'nld', 'pol', 'por', 'ron', 'rus', 'spa', 'swe', 'tur', 'ukr', 'zho']}
"""TVsubtitles Provider."""
languages = {Language('por', 'BR')} | {Language(l) for l in [
'ara', 'bul', 'ces', 'dan', 'deu', 'ell', 'eng', 'fin', 'fra', 'hun', 'ita', 'jpn', 'kor', 'nld', 'pol', 'por',
'ron', 'rus', 'spa', 'swe', 'tur', 'ukr', 'zho'
]}
video_types = (Episode,)
server = 'http://www.tvsubtitles.net'
episode_id_re = re.compile('^episode-\d+\.html$')
subtitle_re = re.compile('^\/subtitle-\d+\.html$')
link_re = re.compile('^(?P<series>.+) \(\d{4}-\d{4}\)$')
server_url = 'http://www.tvsubtitles.net/'
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
self.session = Session()
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
def terminate(self):
self.session.close()
def request(self, url, params=None, data=None, method='GET'):
"""Make a `method` request on `url` with the given parameters
@region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME)
def search_show_id(self, series, year=None):
"""Search the show id from the `series` and `year`.
:param string url: part of the URL to reach with the leading slash
:param dict params: params of the request
:param dict data: data of the request
:param string method: method of the request
:return: the response
:rtype: :class:`bs4.BeautifulSoup`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
:param str series: series of the episode.
:param year: year of the series, if any.
:type year: int
:return: the show id, if any.
:rtype: int
"""
try:
r = self.session.request(method, self.server + url, params=params, data=data, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
return bs4.BeautifulSoup(r.content, ['permissive'])
# make the search
logger.info('Searching show id for %r', series)
r = self.session.post(self.server_url + 'search.php', data={'q': series}, timeout=10)
r.raise_for_status()
@region.cache_on_arguments()
def find_show_id(self, series):
"""Find a show id from the series
:param string series: series of the episode
:return: the show id, if any
:rtype: int or None
"""
data = {'q': series}
logger.debug('Searching series %r', data)
soup = self.request('/search.php', data=data, method='POST')
links = soup.select('div.left li div a[href^="/tvshow-"]')
if not links:
logger.info('Series %r not found', series)
return None
for link in links:
match = self.link_re.match(link.string)
# get the series out of the suggestions
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
show_id = None
for suggestion in soup.select('div.left li div a[href^="/tvshow-"]'):
match = link_re.match(suggestion.text)
if not match:
logger.warning('Could not parse %r', link.string)
logger.error('Failed to match %s', suggestion.text)
continue
if match.group('series').lower().replace('.', ' ').strip() == series.lower():
return int(link['href'][8:-5])
return int(links[0]['href'][8:-5])
@region.cache_on_arguments()
def find_episode_ids(self, show_id, season):
"""Find episode ids from the show id and the season
if match.group('series').lower() == series.lower():
if year is not None and int(match.group('first_year')) != year:
logger.debug('Year does not match')
continue
show_id = int(suggestion['href'][8:-5])
logger.debug('Found show id %d', show_id)
break
:param int show_id: show id
:param int season: season of the episode
:return: episode ids per episode number
return show_id
@region.cache_on_arguments(expiration_time=EPISODE_EXPIRATION_TIME)
def get_episode_ids(self, show_id, season):
"""Get episode ids from the show id and the season.
:param int show_id: show id.
:param int season: season of the episode.
:return: episode ids per episode number.
:rtype: dict
"""
params = {'show_id': show_id, 'season': season}
logger.debug('Searching episodes %r', params)
soup = self.request('/tvshow-{show_id}-{season}.html'.format(**params))
# get the page of the season of the show
logger.info('Getting the page of show id %d, season %d', show_id, season)
r = self.session.get(self.server_url + 'tvshow-%d-%d.html' % (show_id, season), timeout=10)
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
# loop over episode rows
episode_ids = {}
for row in soup.select('table#table5 tr'):
if not row('a', href=self.episode_id_re):
# skip rows that do not have a link to the episode page
if not row('a', href=episode_id_re):
continue
# extract data from the cells
cells = row('td')
episode_ids[int(cells[0].string.split('x')[1])] = int(cells[1].a['href'][8:-5])
episode = int(cells[0].text.split('x')[1])
episode_id = int(cells[1].a['href'][8:-5])
episode_ids[episode] = episode_id
if episode_ids:
logger.debug('Found episode ids %r', episode_ids)
else:
logger.warning('No episode ids found')
return episode_ids
def query(self, series, season, episode):
show_id = self.find_show_id(series.lower())
def query(self, series, season, episode, year=None):
# search the show id
show_id = self.search_show_id(series, year)
if show_id is None:
logger.error('No show id found for %r (%r)', series, {'year': year})
return []
episode_ids = self.find_episode_ids(show_id, season)
# get the episode ids
episode_ids = self.get_episode_ids(show_id, season)
if episode not in episode_ids:
logger.info('Episode %d not found', episode)
logger.error('Episode %d not found', episode)
return []
params = {'episode_id': episode_ids[episode]}
logger.debug('Searching episode %r', params)
soup = self.request('/episode-{episode_id}.html'.format(**params))
return [TVsubtitlesSubtitle(babelfish.Language.fromtvsubtitles(row.h5.img['src'][13:-4]), series, season,
episode, row['href'][10:-5], row.find('p', title='rip').text.strip() or None,
row.find('p', title='release').text.strip() or None)
for row in soup('a', href=self.subtitle_re)]
# get the episode page
logger.info('Getting the page for episode %d', episode_ids[episode])
r = self.session.get(self.server_url + 'episode-%d.html' % episode_ids[episode], timeout=10)
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
# loop over subtitles rows
subtitles = []
for row in soup.select('.subtitlen'):
# read the item
language = Language.fromtvsubtitles(row.h5.img['src'][13:-4])
subtitle_id = int(row.parent['href'][10:-5])
page_link = self.server_url + 'subtitle-%d.html' % subtitle_id
rip = row.find('p', title='rip').text.strip() or None
release = row.find('p', title='release').text.strip() or None
subtitle = TVsubtitlesSubtitle(language, page_link, subtitle_id, series, season, episode, year, rip,
release)
logger.debug('Found subtitle %s', subtitle)
subtitles.append(subtitle)
return subtitles
def list_subtitles(self, video, languages):
return [s for s in self.query(video.series, video.season, video.episode) if s.language in languages]
return [s for s in self.query(video.series, video.season, video.episode, video.year) if s.language in languages]
def download_subtitle(self, subtitle):
try:
r = self.session.get(self.server + '/download-{subtitle_id}.html'.format(subtitle_id=subtitle.id),
timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
with zipfile.ZipFile(io.BytesIO(r.content)) as zf:
# download as a zip
logger.info('Downloading subtitle %r', subtitle)
r = self.session.get(self.server_url + 'download-%d.html' % subtitle.subtitle_id, timeout=10)
r.raise_for_status()
# open the zip
with ZipFile(io.BytesIO(r.content)) as zf:
if len(zf.namelist()) > 1:
raise ProviderError('More than one file to unzip')
subtitle_bytes = zf.read(zf.namelist()[0])
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
subtitle.content = fix_line_ending(zf.read(zf.namelist()[0]))
+12
View File
@@ -0,0 +1,12 @@
"""
Refiners enrich a :class:`~subliminal.video.Video` object by adding information to it.
A refiner is a simple function:
.. py:function:: refine(video, **kwargs)
:param video: the video to refine.
:type video: :class:`~subliminal.video.Video`
:param \*\*kwargs: additional parameters for refiners.
"""
+99
View File
@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
import logging
import os
from babelfish import Error as BabelfishError, Language
from enzyme import MKV
logger = logging.getLogger(__name__)
def refine(video, embedded_subtitles=True, **kwargs):
"""Refine a video by searching its metadata.
Several :class:`~subliminal.video.Video` attributes can be found:
* :attr:`~subliminal.video.Video.resolution`
* :attr:`~subliminal.video.Video.video_codec`
* :attr:`~subliminal.video.Video.audio_codec`
* :attr:`~subliminal.video.Video.subtitle_languages`
:param bool embedded_subtitles: search for embedded subtitles.
"""
# skip non existing videos
if not video.exists:
return
# check extensions
extension = os.path.splitext(video.name)[1]
if extension == '.mkv':
with open(video.name, 'rb') as f:
mkv = MKV(f)
# main video track
if mkv.video_tracks:
video_track = mkv.video_tracks[0]
# resolution
if video_track.height in (480, 720, 1080):
if video_track.interlaced:
video.resolution = '%di' % video_track.height
else:
video.resolution = '%dp' % video_track.height
logger.debug('Found resolution %s', video.resolution)
# video codec
if video_track.codec_id == 'V_MPEG4/ISO/AVC':
video.video_codec = 'h264'
logger.debug('Found video_codec %s', video.video_codec)
elif video_track.codec_id == 'V_MPEG4/ISO/SP':
video.video_codec = 'DivX'
logger.debug('Found video_codec %s', video.video_codec)
elif video_track.codec_id == 'V_MPEG4/ISO/ASP':
video.video_codec = 'XviD'
logger.debug('Found video_codec %s', video.video_codec)
else:
logger.warning('MKV has no video track')
# main audio track
if mkv.audio_tracks:
audio_track = mkv.audio_tracks[0]
# audio codec
if audio_track.codec_id == 'A_AC3':
video.audio_codec = 'AC3'
logger.debug('Found audio_codec %s', video.audio_codec)
elif audio_track.codec_id == 'A_DTS':
video.audio_codec = 'DTS'
logger.debug('Found audio_codec %s', video.audio_codec)
elif audio_track.codec_id == 'A_AAC':
video.audio_codec = 'AAC'
logger.debug('Found audio_codec %s', video.audio_codec)
else:
logger.warning('MKV has no audio track')
# subtitle tracks
if mkv.subtitle_tracks:
if embedded_subtitles:
embedded_subtitle_languages = set()
for st in mkv.subtitle_tracks:
if st.language:
try:
embedded_subtitle_languages.add(Language.fromalpha3b(st.language))
except BabelfishError:
logger.error('Embedded subtitle track language %r is not a valid language', st.language)
embedded_subtitle_languages.add(Language('und'))
elif st.name:
try:
embedded_subtitle_languages.add(Language.fromname(st.name))
except BabelfishError:
logger.debug('Embedded subtitle track name %r is not a valid language', st.name)
embedded_subtitle_languages.add(Language('und'))
else:
embedded_subtitle_languages.add(Language('und'))
logger.debug('Found embedded subtitle %r', embedded_subtitle_languages)
video.subtitle_languages |= embedded_subtitle_languages
else:
logger.debug('MKV has no subtitle track')
else:
logger.debug('Unsupported video extension %s', extension)
+187
View File
@@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
import logging
import operator
import requests
from .. import __short_version__
from ..cache import REFINER_EXPIRATION_TIME, region
from ..video import Episode, Movie
from ..utils import sanitize
logger = logging.getLogger(__name__)
class OMDBClient(object):
base_url = 'http://www.omdbapi.com'
def __init__(self, version=1, session=None, headers=None, timeout=10):
#: Session for the requests
self.session = session or requests.Session()
self.session.timeout = timeout
self.session.headers.update(headers or {})
self.session.params['r'] = 'json'
self.session.params['v'] = version
def get(self, id=None, title=None, type=None, year=None, plot='short', tomatoes=False):
# build the params
params = {}
if id:
params['i'] = id
if title:
params['t'] = title
if not params:
raise ValueError('At least id or title is required')
params['type'] = type
params['y'] = year
params['plot'] = plot
params['tomatoes'] = tomatoes
# perform the request
r = self.session.get(self.base_url, params=params)
r.raise_for_status()
# get the response as json
j = r.json()
# check response status
if j['Response'] == 'False':
return None
return j
def search(self, title, type=None, year=None, page=1):
# build the params
params = {'s': title, 'type': type, 'y': year, 'page': page}
# perform the request
r = self.session.get(self.base_url, params=params)
r.raise_for_status()
# get the response as json
j = r.json()
# check response status
if j['Response'] == 'False':
return None
return j
omdb_client = OMDBClient(headers={'User-Agent': 'Subliminal/%s' % __short_version__})
@region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME)
def search(title, type, year):
results = omdb_client.search(title, type, year)
if not results:
return None
# fetch all paginated results
all_results = results['Search']
total_results = int(results['totalResults'])
page = 1
while total_results > page * 10:
page += 1
results = omdb_client.search(title, type, year, page=page)
all_results.extend(results['Search'])
return all_results
def refine(video, **kwargs):
"""Refine a video by searching `OMDb API <http://omdbapi.com/>`_.
Several :class:`~subliminal.video.Episode` attributes can be found:
* :attr:`~subliminal.video.Episode.series`
* :attr:`~subliminal.video.Episode.year`
* :attr:`~subliminal.video.Episode.series_imdb_id`
Similarly, for a :class:`~subliminal.video.Movie`:
* :attr:`~subliminal.video.Movie.title`
* :attr:`~subliminal.video.Movie.year`
* :attr:`~subliminal.video.Video.imdb_id`
"""
if isinstance(video, Episode):
# exit if the information is complete
if video.series_imdb_id:
logger.debug('No need to search')
return
# search the series
results = search(video.series, 'series', video.year)
if not results:
logger.warning('No results for series')
return
logger.debug('Found %d results', len(results))
# filter the results
results = [r for r in results if sanitize(r['Title']) == sanitize(video.series)]
if not results:
logger.warning('No matching series found')
return
# process the results
found = False
for result in sorted(results, key=operator.itemgetter('Year')):
if video.original_series and video.year is None:
logger.debug('Found result for original series without year')
found = True
break
if video.year == int(result['Year'].split(u'\u2013')[0]):
logger.debug('Found result with matching year')
found = True
break
if not found:
logger.warning('No matching series found')
return
# add series information
logger.debug('Found series %r', result)
video.series = result['Title']
video.year = int(result['Year'].split(u'\u2013')[0])
video.series_imdb_id = result['imdbID']
elif isinstance(video, Movie):
# exit if the information is complete
if video.imdb_id:
return
# search the movie
results = search(video.title, 'movie', video.year)
if not results:
logger.warning('No results')
return
logger.debug('Found %d results', len(results))
# filter the results
results = [r for r in results if sanitize(r['Title']) == sanitize(video.title)]
if not results:
logger.warning('No matching movie found')
return
# process the results
found = False
for result in results:
if video.year is None:
logger.debug('Found result for movie without year')
found = True
break
if video.year == int(result['Year']):
logger.debug('Found result with matching year')
found = True
break
if not found:
logger.warning('No matching movie found')
return
# add movie information
logger.debug('Found movie %r', result)
video.title = result['Title']
video.year = int(result['Year'].split(u'\u2013')[0])
video.imdb_id = result['imdbID']
+350
View File
@@ -0,0 +1,350 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from functools import wraps
import logging
import re
import requests
from .. import __short_version__
from ..cache import REFINER_EXPIRATION_TIME, region
from ..utils import sanitize
from ..video import Episode
logger = logging.getLogger(__name__)
series_re = re.compile(r'^(?P<series>.*?)(?: \((?:(?P<year>\d{4})|(?P<country>[A-Z]{2}))\))?$')
def requires_auth(func):
"""Decorator for :class:`TVDBClient` methods that require authentication"""
@wraps(func)
def wrapper(self, *args, **kwargs):
if self.token is None or self.token_expired:
self.login()
elif self.token_needs_refresh:
self.refresh_token()
return func(self, *args, **kwargs)
return wrapper
class TVDBClient(object):
"""TVDB REST API Client
:param str apikey: API key to use.
:param str username: username to use.
:param str password: password to use.
:param str language: language of the responses.
:param session: session object to use.
:type session: :class:`requests.sessions.Session` or compatible.
:param dict headers: additional headers.
:param int timeout: timeout for the requests.
"""
#: Base URL of the API
base_url = 'https://api.thetvdb.com'
#: Token lifespan
token_lifespan = timedelta(hours=1)
#: Minimum token age before a :meth:`refresh_token` is triggered
refresh_token_every = timedelta(minutes=30)
def __init__(self, apikey=None, username=None, password=None, language='en', session=None, headers=None,
timeout=10):
#: API key
self.apikey = apikey
#: Username
self.username = username
#: Password
self.password = password
#: Last token acquisition date
self.token_date = datetime.utcnow() - self.token_lifespan
#: Session for the requests
self.session = session or requests.Session()
self.session.timeout = timeout
self.session.headers.update(headers or {})
self.session.headers['Content-Type'] = 'application/json'
self.session.headers['Accept-Language'] = language
@property
def language(self):
return self.session.headers['Accept-Language']
@language.setter
def language(self, value):
self.session.headers['Accept-Language'] = value
@property
def token(self):
if 'Authorization' not in self.session.headers:
return None
return self.session.headers['Authorization'][7:]
@property
def token_expired(self):
return datetime.utcnow() - self.token_date > self.token_lifespan
@property
def token_needs_refresh(self):
return datetime.utcnow() - self.token_date > self.refresh_token_every
def login(self):
"""Login"""
# perform the request
data = {'apikey': self.apikey, 'username': self.username, 'password': self.password}
r = self.session.post(self.base_url + '/login', json=data)
r.raise_for_status()
# set the Authorization header
self.session.headers['Authorization'] = 'Bearer ' + r.json()['token']
# update token_date
self.token_date = datetime.utcnow()
def refresh_token(self):
"""Refresh token"""
# perform the request
r = self.session.get(self.base_url + '/refresh_token')
r.raise_for_status()
# set the Authorization header
self.session.headers['Authorization'] = 'Bearer ' + r.json()['token']
# update token_date
self.token_date = datetime.utcnow()
@requires_auth
def search_series(self, name=None, imdb_id=None, zap2it_id=None):
"""Search series"""
# perform the request
params = {'name': name, 'imdbId': imdb_id, 'zap2itId': zap2it_id}
r = self.session.get(self.base_url + '/search/series', params=params)
if r.status_code == 404:
return None
r.raise_for_status()
return r.json()['data']
@requires_auth
def get_series(self, id):
"""Get series"""
# perform the request
r = self.session.get(self.base_url + '/series/{}'.format(id))
if r.status_code == 404:
return None
r.raise_for_status()
return r.json()['data']
@requires_auth
def get_series_actors(self, id):
"""Get series actors"""
# perform the request
r = self.session.get(self.base_url + '/series/{}/actors'.format(id))
if r.status_code == 404:
return None
r.raise_for_status()
return r.json()['data']
@requires_auth
def get_series_episodes(self, id, page=1):
"""Get series episodes"""
# perform the request
params = {'page': page}
r = self.session.get(self.base_url + '/series/{}/episodes'.format(id), params=params)
if r.status_code == 404:
return None
r.raise_for_status()
return r.json()
@requires_auth
def query_series_episodes(self, id, absolute_number=None, aired_season=None, aired_episode=None, dvd_season=None,
dvd_episode=None, imdb_id=None, page=1):
"""Query series episodes"""
# perform the request
params = {'absoluteNumber': absolute_number, 'airedSeason': aired_season, 'airedEpisode': aired_episode,
'dvdSeason': dvd_season, 'dvdEpisode': dvd_episode, 'imdbId': imdb_id, 'page': page}
r = self.session.get(self.base_url + '/series/{}/episodes/query'.format(id), params=params)
if r.status_code == 404:
return None
r.raise_for_status()
return r.json()
@requires_auth
def get_episode(self, id):
"""Get episode"""
# perform the request
r = self.session.get(self.base_url + '/episodes/{}'.format(id))
if r.status_code == 404:
return None
r.raise_for_status()
return r.json()['data']
#: Configured instance of :class:`TVDBClient`
tvdb_client = TVDBClient('5EC930FB90DA1ADA', headers={'User-Agent': 'Subliminal/%s' % __short_version__})
@region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME)
def search_series(name):
"""Search series.
:param str name: name of the series.
:return: the search results.
:rtype: list
"""
return tvdb_client.search_series(name)
@region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME)
def get_series(id):
"""Get series.
:param int id: id of the series.
:return: the series data.
:rtype: dict
"""
return tvdb_client.get_series(id)
@region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME)
def get_series_episode(series_id, season, episode):
"""Get an episode of a series.
:param int series_id: id of the series.
:param int season: season number of the episode.
:param int episode: episode number of the episode.
:return: the episode data.
:rtype: dict
"""
result = tvdb_client.query_series_episodes(series_id, aired_season=season, aired_episode=episode)
if result:
return tvdb_client.get_episode(result['data'][0]['id'])
def refine(video, **kwargs):
"""Refine a video by searching `TheTVDB <http://thetvdb.com/>`_.
.. note::
This refiner only work for instances of :class:`~subliminal.video.Episode`.
Several attributes can be found:
* :attr:`~subliminal.video.Episode.series`
* :attr:`~subliminal.video.Episode.year`
* :attr:`~subliminal.video.Episode.series_imdb_id`
* :attr:`~subliminal.video.Episode.series_tvdb_id`
* :attr:`~subliminal.video.Episode.title`
* :attr:`~subliminal.video.Video.imdb_id`
* :attr:`~subliminal.video.Episode.tvdb_id`
"""
# only deal with Episode videos
if not isinstance(video, Episode):
logger.error('Cannot refine episodes')
return
# exit if the information is complete
if video.series_tvdb_id and video.tvdb_id:
logger.debug('No need to search')
return
# search the series
logger.info('Searching series %r', video.series)
results = search_series(video.series.lower())
if not results:
logger.warning('No results for series')
return
logger.debug('Found %d results', len(results))
# search for exact matches
matching_results = []
for result in results:
matching_result = {}
# use seriesName and aliases
series_names = [result['seriesName']]
series_names.extend(result['aliases'])
# parse the original series as series + year or country
original_match = series_re.match(result['seriesName']).groupdict()
# parse series year
series_year = None
if result['firstAired']:
series_year = datetime.strptime(result['firstAired'], '%Y-%m-%d').year
# discard mismatches on year
if video.year and series_year and video.year != series_year:
logger.debug('Discarding series %r mismatch on year %d', result['seriesName'], series_year)
continue
# iterate over series names
for series_name in series_names:
# parse as series and year
series, year, country = series_re.match(series_name).groups()
if year:
year = int(year)
# discard mismatches on year
if year and (video.original_series or video.year != year):
logger.debug('Discarding series name %r mismatch on year %d', series, year)
continue
# match on sanitized series name
if sanitize(series) == sanitize(video.series):
logger.debug('Found exact match on series %r', series_name)
matching_result['match'] = {'series': original_match['series'], 'year': series_year,
'original_series': original_match['year'] is None}
break
# add the result on match
if matching_result:
matching_result['data'] = result
matching_results.append(matching_result)
# exit if we don't have exactly 1 matching result
if not matching_results:
logger.error('No matching series found')
return
if len(matching_results) > 1:
logger.error('Multiple matches found')
return
# get the series
matching_result = matching_results[0]
series = get_series(matching_result['data']['id'])
# add series information
logger.debug('Found series %r', series)
video.series = matching_result['match']['series']
video.year = matching_result['match']['year']
video.original_series = matching_result['match']['original_series']
video.series_tvdb_id = series['id']
video.series_imdb_id = series['imdbId'] or None
# get the episode
logger.info('Getting series episode %dx%d', video.season, video.episode)
episode = get_series_episode(video.series_tvdb_id, video.season, video.episode)
if not episode:
logger.warning('No results for episode')
return
# add episode information
logger.debug('Found episode %r', episode)
video.tvdb_id = episode['id']
video.title = episode['episodeName'] or None
video.imdb_id = episode['imdbId'] or None
+196 -67
View File
@@ -1,84 +1,213 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals
from sympy import Eq, symbols, solve
"""
This module provides the default implementation of the `compute_score` parameter in
:meth:`~subliminal.core.ProviderPool.download_best_subtitles` and :func:`~subliminal.core.download_best_subtitles`.
.. note::
To avoid unnecessary dependency on `sympy <http://www.sympy.org/>`_ and boost subliminal's import time, the
resulting scores are hardcoded here and manually updated when the set of equations change.
Available matches:
* hash
* title
* year
* series
* season
* episode
* release_group
* format
* audio_codec
* resolution
* hearing_impaired
* video_codec
* series_imdb_id
* imdb_id
* tvdb_id
"""
from __future__ import division, print_function
import logging
from .video import Episode, Movie
logger = logging.getLogger(__name__)
# Symbols
release_group, resolution, video_codec, audio_codec = symbols('release_group resolution video_codec audio_codec')
imdb_id, hash, title, series, tvdb_id, season, episode = symbols('imdb_id hash title series tvdb_id season episode')
year = symbols('year')
#: Scores for episodes
episode_scores = {'hash': 359, 'series': 180, 'year': 90, 'season': 30, 'episode': 30, 'release_group': 15,
'format': 7, 'audio_codec': 3, 'resolution': 2, 'video_codec': 2, 'hearing_impaired': 1}
#: Scores for movies
movie_scores = {'hash': 119, 'title': 60, 'year': 30, 'release_group': 15,
'format': 7, 'audio_codec': 3, 'resolution': 2, 'video_codec': 2, 'hearing_impaired': 1}
def get_episode_equations():
"""Get the score equations for a :class:`~subliminal.video.Episode`
def get_scores(video):
"""Get the scores dict for the given `video`.
The equations are the following:
This will return either :data:`episode_scores` or :data:`movie_scores` based on the type of the `video`.
1. hash = resolution + video_codec + audio_codec + series + season + episode + release_group
2. series = resolution + video_codec + audio_codec + season + episode + 1
3. tvdb_id = series
4. season = resolution + video_codec + audio_codec + 1
5. imdb_id = series + season + episode
6. resolution = video_codec
7. video_codec = 2 * audio_codec
8. title = season + episode
9. season = episode
10. release_group = season
11. audio_codec = 1
:return: the score equations for an episode
:rtype: list of :class:`sympy.Eq`
:param video: the video to compute the score against.
:type video: :class:`~subliminal.video.Video`
:return: the scores dict.
:rtype: dict
"""
equations = []
equations.append(Eq(hash, resolution + video_codec + audio_codec + series + season + episode + release_group))
equations.append(Eq(series, resolution + video_codec + audio_codec + season + episode + release_group))
equations.append(Eq(tvdb_id, series))
equations.append(Eq(season, resolution + video_codec + audio_codec + 1))
equations.append(Eq(imdb_id, series + season + episode))
equations.append(Eq(resolution, video_codec))
equations.append(Eq(video_codec, 2 * audio_codec))
equations.append(Eq(title, season + episode))
equations.append(Eq(season, episode))
equations.append(Eq(release_group, season))
equations.append(Eq(audio_codec, 1))
return equations
if isinstance(video, Episode):
return episode_scores
elif isinstance(video, Movie):
return movie_scores
raise ValueError('video must be an instance of Episode or Movie')
def get_movie_equations():
"""Get the score equations for a :class:`~subliminal.video.Movie`
def compute_score(subtitle, video, hearing_impaired=None):
"""Compute the score of the `subtitle` against the `video` with `hearing_impaired` preference.
The equations are the following:
:func:`compute_score` uses the :meth:`Subtitle.get_matches <subliminal.subtitle.Subtitle.get_matches>` method and
applies the scores (either from :data:`episode_scores` or :data:`movie_scores`) after some processing.
1. hash = resolution + video_codec + audio_codec + title + year + release_group
2. imdb_id = hash
3. resolution = video_codec
4. video_codec = 2 * audio_codec
5. title = resolution + video_codec + audio_codec + year + 1
6. release_group = resolution + video_codec + audio_codec + 1
7. year = release_group + 1
8. audio_codec = 1
:return: the score equations for a movie
:rtype: list of :class:`sympy.Eq`
:param subtitle: the subtitle to compute the score of.
:type subtitle: :class:`~subliminal.subtitle.Subtitle`
:param video: the video to compute the score against.
:type video: :class:`~subliminal.video.Video`
:param bool hearing_impaired: hearing impaired preference.
:return: score of the subtitle.
:rtype: int
"""
equations = []
equations.append(Eq(hash, resolution + video_codec + audio_codec + title + year + release_group))
equations.append(Eq(imdb_id, hash))
equations.append(Eq(resolution, video_codec))
equations.append(Eq(video_codec, 2 * audio_codec))
equations.append(Eq(title, resolution + video_codec + audio_codec + year + 1))
equations.append(Eq(video_codec, 2 * audio_codec))
equations.append(Eq(release_group, resolution + video_codec + audio_codec + 1))
equations.append(Eq(year, release_group + 1))
equations.append(Eq(audio_codec, 1))
return equations
logger.info('Computing score of %r for video %r with %r', subtitle, video, dict(hearing_impaired=hearing_impaired))
# get the scores dict
scores = get_scores(video)
logger.debug('Using scores %r', scores)
# get the matches
matches = subtitle.get_matches(video)
logger.debug('Found matches %r', matches)
# on hash match, discard everything else
if 'hash' in matches:
logger.debug('Keeping only hash match')
matches &= {'hash'}
# handle equivalent matches
if isinstance(video, Episode):
if 'title' in matches:
logger.debug('Adding title match equivalent')
matches.add('episode')
if 'series_imdb_id' in matches:
logger.debug('Adding series_imdb_id match equivalent')
matches |= {'series', 'year'}
if 'imdb_id' in matches:
logger.debug('Adding imdb_id match equivalents')
matches |= {'series', 'year', 'season', 'episode'}
if 'tvdb_id' in matches:
logger.debug('Adding tvdb_id match equivalents')
matches |= {'series', 'year'}
elif isinstance(video, Movie):
if 'imdb_id' in matches:
logger.debug('Adding imdb_id match equivalents')
matches |= {'title', 'year'}
# handle hearing impaired
if hearing_impaired is not None and subtitle.hearing_impaired == hearing_impaired:
logger.debug('Matched hearing_impaired')
matches.add('hearing_impaired')
# compute the score
score = sum((scores.get(match, 0) for match in matches))
logger.info('Computed score %r with final matches %r', score, matches)
# ensure score is within valid bounds
assert 0 <= score <= scores['hash'] + scores['hearing_impaired']
return score
if __name__ == '__main__':
print(solve(get_episode_equations(), [release_group, resolution, video_codec, audio_codec, imdb_id,
hash, series, tvdb_id, season, episode, title]))
print(solve(get_movie_equations(), [release_group, resolution, video_codec, audio_codec, imdb_id,
hash, title, year]))
def solve_episode_equations():
from sympy import Eq, solve, symbols
hash, series, year, season, episode, release_group = symbols('hash series year season episode release_group')
format, audio_codec, resolution, video_codec = symbols('format audio_codec resolution video_codec')
hearing_impaired = symbols('hearing_impaired')
equations = [
# hash is best
Eq(hash, series + year + season + episode + release_group + format + audio_codec + resolution + video_codec),
# series counts for the most part in the total score
Eq(series, year + season + episode + release_group + format + audio_codec + resolution + video_codec + 1),
# year is the second most important part
Eq(year, season + episode + release_group + format + audio_codec + resolution + video_codec + 1),
# season is important too
Eq(season, release_group + format + audio_codec + resolution + video_codec + 1),
# episode is equally important to season
Eq(episode, season),
# release group is the next most wanted match
Eq(release_group, format + audio_codec + resolution + video_codec + 1),
# format counts as much as audio_codec, resolution and video_codec
Eq(format, audio_codec + resolution + video_codec),
# audio_codec is more valuable than video_codec
Eq(audio_codec, video_codec + 1),
# resolution counts as much as video_codec
Eq(resolution, video_codec),
# video_codec is the least valuable match but counts more than the sum of all scoring increasing matches
Eq(video_codec, hearing_impaired + 1),
# hearing impaired is only used for score increasing, so put it to 1
Eq(hearing_impaired, 1),
]
return solve(equations, [hash, series, year, season, episode, release_group, format, audio_codec, resolution,
hearing_impaired, video_codec])
def solve_movie_equations():
from sympy import Eq, solve, symbols
hash, title, year, release_group = symbols('hash title year release_group')
format, audio_codec, resolution, video_codec = symbols('format audio_codec resolution video_codec')
hearing_impaired = symbols('hearing_impaired')
equations = [
# hash is best
Eq(hash, title + year + release_group + format + audio_codec + resolution + video_codec),
# title counts for the most part in the total score
Eq(title, year + release_group + format + audio_codec + resolution + video_codec + 1),
# year is the second most important part
Eq(year, release_group + format + audio_codec + resolution + video_codec + 1),
# release group is the next most wanted match
Eq(release_group, format + audio_codec + resolution + video_codec + 1),
# format counts as much as audio_codec, resolution and video_codec
Eq(format, audio_codec + resolution + video_codec),
# audio_codec is more valuable than video_codec
Eq(audio_codec, video_codec + 1),
# resolution counts as much as video_codec
Eq(resolution, video_codec),
# video_codec is the least valuable match but counts more than the sum of all scoring increasing matches
Eq(video_codec, hearing_impaired + 1),
# hearing impaired is only used for score increasing, so put it to 1
Eq(hearing_impaired, 1),
]
return solve(equations, [hash, title, year, release_group, format, audio_codec, resolution, hearing_impaired,
video_codec])
+203 -106
View File
@@ -1,156 +1,253 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import codecs
import logging
import os.path
import babelfish
import os
import chardet
import pysrt
from .video import Episode, Movie
from .utils import sanitize, sanitize_release_group
logger = logging.getLogger(__name__)
#: Subtitle extensions
SUBTITLE_EXTENSIONS = ('.srt', '.sub', '.smi', '.txt', '.ssa', '.ass', '.mpl')
class Subtitle(object):
"""Base class for subtitle
"""Base class for subtitle.
:param language: language of the subtitle
:type language: :class:`babelfish.Language`
:param bool hearing_impaired: `True` if the subtitle is hearing impaired, `False` otherwise
:param language: language of the subtitle.
:type language: :class:`~babelfish.language.Language`
:param bool hearing_impaired: whether or not the subtitle is hearing impaired.
:param page_link: URL of the web page from which the subtitle can be downloaded.
:type page_link: str
:param encoding: Text encoding of the subtitle.
:type encoding: str
"""
def __init__(self, language, hearing_impaired=False):
#: Name of the provider that returns that class of subtitle
provider_name = ''
def __init__(self, language, hearing_impaired=False, page_link=None, encoding=None):
#: Language of the subtitle
self.language = language
#: Whether or not the subtitle is hearing impaired
self.hearing_impaired = hearing_impaired
def compute_matches(self, video):
"""Compute the matches of the subtitle against the `video`
#: URL of the web page from which the subtitle can be downloaded
self.page_link = page_link
:param video: the video to compute the matches against
#: Content as bytes
self.content = None
#: Encoding to decode with when accessing :attr:`text`
self.encoding = None
# validate the encoding
if encoding:
try:
self.encoding = codecs.lookup(encoding).name
except (TypeError, LookupError):
logger.debug('Unsupported encoding %s', encoding)
@property
def id(self):
"""Unique identifier of the subtitle"""
raise NotImplementedError
@property
def text(self):
"""Content as string
If :attr:`encoding` is None, the encoding is guessed with :meth:`guess_encoding`
"""
if not self.content:
return
if self.encoding:
return self.content.decode(self.encoding, errors='replace')
return self.content.decode(self.guess_encoding(), errors='replace')
def is_valid(self):
"""Check if a :attr:`text` is a valid SubRip format.
:return: whether or not the subtitle is valid.
:rtype: bool
"""
if not self.text:
return False
try:
pysrt.from_string(self.text, error_handling=pysrt.ERROR_RAISE)
except pysrt.Error as e:
if e.args[0] < 80:
return False
return True
def guess_encoding(self):
"""Guess encoding using the language, falling back on chardet.
:return: the guessed encoding.
:rtype: str
"""
logger.info('Guessing encoding for language %s', self.language)
# always try utf-8 first
encodings = ['utf-8']
# add language-specific encodings
if self.language.alpha3 == 'zho':
encodings.extend(['gb18030', 'big5'])
elif self.language.alpha3 == 'jpn':
encodings.append('shift-jis')
elif self.language.alpha3 == 'ara':
encodings.append('windows-1256')
elif self.language.alpha3 == 'heb':
encodings.append('windows-1255')
elif self.language.alpha3 == 'tur':
encodings.extend(['iso-8859-9', 'windows-1254'])
elif self.language.alpha3 == 'pol':
# Eastern European Group 1
encodings.extend(['windows-1250'])
elif self.language.alpha3 == 'bul':
# Eastern European Group 2
encodings.extend(['windows-1251'])
else:
# Western European (windows-1252)
encodings.append('latin-1')
# try to decode
logger.debug('Trying encodings %r', encodings)
for encoding in encodings:
try:
self.content.decode(encoding)
except UnicodeDecodeError:
pass
else:
logger.info('Guessed encoding %s', encoding)
return encoding
logger.warning('Could not guess encoding from language')
# fallback on chardet
encoding = chardet.detect(self.content)['encoding']
logger.info('Chardet found encoding %s', encoding)
return encoding
def get_matches(self, video):
"""Get the matches against the `video`.
:param video: the video to get the matches with.
:type video: :class:`~subliminal.video.Video`
:return: matches of the subtitle
:return: matches of the subtitle.
:rtype: set
"""
raise NotImplementedError
def compute_score(self, video):
"""Compute the score of the subtitle against the `video`
There are equivalent matches so that a provider can match one element or its equivalent. This is
to give all provider a chance to have a score in the same range without hurting quality.
* Matching :class:`~subliminal.video.Video`'s `hashes` is equivalent to matching everything else
* Matching :class:`~subliminal.video.Episode`'s `season` and `episode`
is equivalent to matching :class:`~subliminal.video.Episode`'s `title`
* Matching :class:`~subliminal.video.Episode`'s `tvdb_id` is equivalent to matching
:class:`~subliminal.video.Episode`'s `series`
:param video: the video to compute the score against
:type video: :class:`~subliminal.video.Video`
:return: score of the subtitle
:rtype: int
"""
score = 0
# compute matches
initial_matches = self.compute_matches(video)
matches = initial_matches.copy()
# hash is the perfect match
if 'hash' in matches:
score = video.scores['hash']
else:
# remove equivalences
if isinstance(video, Episode):
if 'imdb_id' in matches:
matches -= {'series', 'tvdb_id', 'season', 'episode', 'title'}
if 'tvdb_id' in matches:
matches -= {'series'}
if 'title' in matches:
matches -= {'season', 'episode'}
# add other scores
score += sum((video.scores[match] for match in matches))
logger.info('Computed score %d with matches %r', score, initial_matches)
return score
def __hash__(self):
return hash(self.provider_name + '-' + self.id)
def __repr__(self):
return '<%s [%s]>' % (self.__class__.__name__, self.language)
return '<%s %r [%s]>' % (self.__class__.__name__, self.id, self.language)
def get_subtitle_path(video_path, language=None):
"""Create the subtitle path from the given `video_path` and `language`
def get_subtitle_path(video_path, language=None, extension='.srt'):
"""Get the subtitle path using the `video_path` and `language`.
:param string video_path: path to the video
:param language: language of the subtitle to put in the path
:type language: :class:`babelfish.Language` or None
:return: path of the subtitle
:rtype: string
:param str video_path: path to the video.
:param language: language of the subtitle to put in the path.
:type language: :class:`~babelfish.language.Language`
:param str extension: extension of the subtitle.
:return: path of the subtitle.
:rtype: str
"""
subtitle_path = os.path.splitext(video_path)[0]
if language is not None:
try:
return subtitle_path + '.%s.%s' % (language.alpha2, 'srt')
except babelfish.ConvertError:
return subtitle_path + '.%s.%s' % (language.alpha3, 'srt')
return subtitle_path + '.srt'
subtitle_root = os.path.splitext(video_path)[0]
if language:
subtitle_root += '.' + str(language)
return subtitle_root + extension
def is_valid_subtitle(subtitle_text):
"""Check if a subtitle text is a valid SubRip format
def guess_matches(video, guess, partial=False):
"""Get matches between a `video` and a `guess`.
:return: `True` if the subtitle is valid, `False` otherwise
:rtype: bool
If a guess is `partial`, the absence information won't be counted as a match.
"""
try:
pysrt.from_string(subtitle_text, error_handling=pysrt.ERROR_RAISE)
return True
except pysrt.Error:
pass
except:
logger.exception('Unexpected error when validating subtitle')
return False
def compute_guess_matches(video, guess):
"""Compute matches between a `video` and a `guess`
:param video: the video to compute the matches on
:param video: the video.
:type video: :class:`~subliminal.video.Video`
:param guess: the guess to compute the matches on
:type guess: :class:`guessit.Guess`
:return: matches of the `guess`
:param guess: the guess.
:type guess: dict
:param bool partial: whether or not the guess is partial.
:return: matches between the `video` and the `guess`.
:rtype: set
"""
matches = set()
if isinstance(video, Episode):
# Series
if video.series and 'series' in guess and guess['series'].lower() == video.series.lower():
# series
if video.series and 'title' in guess and sanitize(guess['title']) == sanitize(video.series):
matches.add('series')
# Season
if video.season and 'seasonNumber' in guess and guess['seasonNumber'] == video.season:
# title
if video.title and 'episode_title' in guess and sanitize(guess['episode_title']) == sanitize(video.title):
matches.add('title')
# season
if video.season and 'season' in guess and guess['season'] == video.season:
matches.add('season')
# Episode
if video.episode and 'episodeNumber' in guess and guess['episodeNumber'] == video.episode:
# episode
if video.episode and 'episode' in guess and guess['episode'] == video.episode:
matches.add('episode')
elif isinstance(video, Movie):
# Year
# year
if video.year and 'year' in guess and guess['year'] == video.year:
matches.add('year')
# Title
if video.title and 'title' in guess and guess['title'].lower() == video.title.lower():
matches.add('title')
# Release group
if video.release_group and 'releaseGroup' in guess and guess['releaseGroup'].lower() == video.release_group.lower():
# count "no year" as an information
if not partial and video.original_series and 'year' not in guess:
matches.add('year')
elif isinstance(video, Movie):
# year
if video.year and 'year' in guess and guess['year'] == video.year:
matches.add('year')
# title
if video.title and 'title' in guess and sanitize(guess['title']) == sanitize(video.title):
matches.add('title')
# release_group
if (video.release_group and 'release_group' in guess and
sanitize_release_group(guess['release_group']) == sanitize_release_group(video.release_group)):
matches.add('release_group')
# Screen size
if video.resolution and 'screenSize' in guess and guess['screenSize'] == video.resolution:
# resolution
if video.resolution and 'screen_size' in guess and guess['screen_size'] == video.resolution:
matches.add('resolution')
# Video codec
if video.video_codec and 'videoCodec' in guess and guess['videoCodec'] == video.video_codec:
# format
if video.format and 'format' in guess and guess['format'].lower() == video.format.lower():
matches.add('format')
# video_codec
if video.video_codec and 'video_codec' in guess and guess['video_codec'] == video.video_codec:
matches.add('video_codec')
# Audio codec
if video.audio_codec and 'audioCodec' in guess and guess['audioCodec'] == video.audio_codec:
# audio_codec
if video.audio_codec and 'audio_codec' in guess and guess['audio_codec'] == video.audio_codec:
matches.add('audio_codec')
return matches
def fix_line_ending(content):
"""Fix line ending of `content` by changing it to \n.
:param bytes content: content of the subtitle.
:return: the content with fixed line endings.
:rtype: bytes
"""
return content.replace(b'\r\n', b'\n').replace(b'\r', b'\n')
-14
View File
@@ -1,14 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from unittest import TextTestRunner, TestSuite
from subliminal import cache_region
from . import test_providers, test_subliminal
cache_region.configure('dogpile.cache.memory', expiration_time=60 * 30)
suite = TestSuite([test_providers.suite(), test_subliminal.suite()])
if __name__ == '__main__':
TextTestRunner().run(suite)
-19
View File
@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from subliminal import Movie, Episode
MOVIES = [Movie('Man of Steel (2013)/man.of.steel.2013.720p.bluray.x264-felony.mkv', 'Man of Steel',
release_group='felony', resolution='720p', video_codec='h264', audio_codec='DTS', imdb_id=770828,
size=7033732714, year=2013,
hashes={'opensubtitles': '5b8f8f4e41ccb21e', 'thesubdb': 'ad32876133355929d814457537e12dc2'})]
EPISODES = [Episode('The Big Bang Theory/Season 07/The.Big.Bang.Theory.S07E05.720p.HDTV.X264-DIMENSION.mkv',
'The Big Bang Theory', 7, 5, release_group='DIMENSION', resolution='720p', video_codec='h264',
audio_codec='AC3', imdb_id=3229392, size=501910737, title='The Workplace Proximity',
tvdb_id=80379,
hashes={'opensubtitles': '6878b3ef7c1bd19e', 'thesubdb': '9dbbfb7ba81c9a6237237dae8589fccc'}),
Episode('Game of Thrones/Season 03/Game.of.Thrones.S03E10.Mhysa.720p.WEB-DL.DD5.1.H.264-NTb.mkv',
'Game of Thrones', 3, 10, release_group='NTb', resolution='720p', video_codec='h264',
audio_codec='AC3', imdb_id=2178796, size=2142810931, title='Mhysa', tvdb_id=121361,
hashes={'opensubtitles': 'b850baa096976c22', 'thesubdb': 'b1f899c77f4c960b84b8dbf840d4e42d'})]
-448
View File
@@ -1,448 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
from unittest import TestCase, TestSuite, TestLoader, TextTestRunner
from babelfish import Language
from pkg_resources import iter_entry_points
from subliminal import PROVIDERS_ENTRY_POINT
from subliminal.subtitle import is_valid_subtitle
from subliminal.tests.common import MOVIES, EPISODES
class ProviderTestCase(TestCase):
provider_name = ''
def setUp(self):
for provider_entry_point in iter_entry_points(PROVIDERS_ENTRY_POINT, self.provider_name):
self.Provider = provider_entry_point.load()
break
class Addic7edProviderTestCase(ProviderTestCase):
provider_name = 'addic7ed'
def test_find_show_id(self):
with self.Provider() as provider:
show_id = provider.find_show_id('The Big Bang')
self.assertTrue(show_id == 126)
def test_find_show_id_error(self):
with self.Provider() as provider:
show_id = provider.find_show_id('the big how i met your mother')
self.assertTrue(show_id is None)
def test_get_show_ids(self):
with self.Provider() as provider:
show_ids = provider.get_show_ids()
self.assertTrue('the big bang theory' in show_ids and show_ids['the big bang theory'] == 126)
def test_query_episode_0(self):
video = EPISODES[0]
languages = {Language('tur'), Language('rus'), Language('heb'), Language('ita'), Language('fra'),
Language('ron'), Language('nld'), Language('eng'), Language('deu'), Language('ell'),
Language('por', 'BR'), Language('bul')}
matches = {frozenset(['episode', 'release_group', 'title', 'series', 'resolution', 'season']),
frozenset(['series', 'resolution', 'season']),
frozenset(['series', 'episode', 'season', 'title']),
frozenset(['series', 'release_group', 'season']),
frozenset(['series', 'episode', 'season', 'release_group', 'title']),
frozenset(['series', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1(self):
video = EPISODES[1]
languages = {Language('ind'), Language('spa'), Language('hrv'), Language('ita'), Language('fra'),
Language('cat'), Language('ell'), Language('nld'), Language('eng'), Language('fas'),
Language('por'), Language('nor'), Language('deu'), Language('ron'), Language('por', 'BR'),
Language('bul')}
matches = {frozenset(['series', 'episode', 'resolution', 'season', 'title']),
frozenset(['series', 'resolution', 'season']),
frozenset(['series', 'episode', 'season', 'title']),
frozenset(['series', 'release_group', 'season']),
frozenset(['series', 'resolution', 'release_group', 'season']),
frozenset(['series', 'episode', 'season', 'release_group', 'title']),
frozenset(['series', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = EPISODES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['series', 'episode', 'season', 'release_group', 'title']),
frozenset(['series', 'episode', 'season', 'title'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = EPISODES[0]
languages = {Language('eng'), Language('fra')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class BierDopjeProviderTestCase(ProviderTestCase):
provider_name = 'bierdopje'
def test_find_show_id(self):
with self.Provider() as provider:
show_id = provider.find_show_id('The Big Bang')
self.assertTrue(show_id == 9203)
def test_find_show_id_error(self):
with self.Provider() as provider:
show_id = provider.find_show_id('the big how i met your mother')
self.assertTrue(show_id is None)
def test_query_episode_0(self):
video = EPISODES[0]
language = Language('eng')
matches = {frozenset(['series', 'video_codec', 'resolution', 'episode', 'season']),
frozenset(['season', 'video_codec', 'episode', 'series']),
frozenset(['episode', 'video_codec', 'season', 'series', 'resolution', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.query(language, video.season, video.episode, series=video.series)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_query_episode_1(self):
video = EPISODES[1]
language = Language('nld')
matches = {frozenset(['series', 'video_codec', 'resolution', 'episode', 'season']),
frozenset(['season', 'video_codec', 'episode', 'series']),
frozenset(['series', 'episode', 'season']),
frozenset(['season', 'video_codec', 'episode', 'release_group', 'series']),
frozenset(['episode', 'video_codec', 'season', 'series', 'resolution', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.query(language, video.season, video.episode, series=video.series)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_query_episode_0_tvdb_id(self):
video = EPISODES[0]
language = Language('eng')
matches = {frozenset(['video_codec', 'tvdb_id', 'episode', 'season', 'series']),
frozenset(['episode', 'video_codec', 'series', 'season', 'tvdb_id', 'resolution', 'release_group']),
frozenset(['episode', 'series', 'video_codec', 'tvdb_id', 'resolution', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(language, video.season, video.episode, tvdb_id=video.tvdb_id)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_list_subtitles(self):
video = EPISODES[1]
languages = {Language('eng'), Language('nld')}
matches = {frozenset(['series', 'video_codec', 'tvdb_id', 'episode', 'season']),
frozenset(['episode', 'video_codec', 'season', 'series', 'tvdb_id', 'resolution', 'release_group']),
frozenset(['season', 'tvdb_id', 'episode', 'series']),
frozenset(['episode', 'video_codec', 'season', 'series', 'tvdb_id', 'resolution']),
frozenset(['episode', 'video_codec', 'season', 'series', 'tvdb_id', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = EPISODES[0]
languages = {Language('eng'), Language('nld')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class OpenSubtitlesProviderTestCase(ProviderTestCase):
provider_name = 'opensubtitles'
def test_query_movie_0_query(self):
video = MOVIES[0]
languages = {Language('eng')}
matches = {frozenset([]), frozenset(['imdb_id', 'resolution', 'title', 'year']),
frozenset(['imdb_id', 'title', 'year']),
frozenset(['imdb_id', 'video_codec', 'title', 'year']),
frozenset(['imdb_id', 'resolution', 'title', 'video_codec', 'year']),
frozenset(['imdb_id', 'title', 'year', 'video_codec', 'resolution', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.query(languages, query=video.title)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_0_query(self):
video = EPISODES[0]
languages = {Language('eng')}
matches = {frozenset(['series', 'episode', 'season', 'imdb_id']),
frozenset(['series', 'imdb_id', 'video_codec', 'episode', 'season']),
frozenset(['episode', 'title', 'series', 'imdb_id', 'video_codec', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(languages, query=video.name.split(os.sep)[-1])
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1_query(self):
video = EPISODES[1]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['episode', 'title', 'series', 'imdb_id', 'video_codec', 'season']),
frozenset(['series', 'imdb_id', 'title', 'episode', 'season']),
frozenset(['series', 'imdb_id', 'video_codec', 'episode', 'season']),
frozenset(['episode', 'video_codec', 'series', 'imdb_id', 'resolution', 'season']),
frozenset(['series', 'imdb_id', 'resolution', 'episode', 'season']),
frozenset(['series', 'episode', 'season', 'imdb_id'])}
with self.Provider() as provider:
subtitles = provider.query(languages, query=video.name.split(os.sep)[-1])
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_movie_0_imdb_id(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['imdb_id', 'video_codec', 'title', 'year']),
frozenset(['imdb_id', 'resolution', 'title', 'video_codec', 'year']),
frozenset(['imdb_id', 'title', 'year', 'video_codec', 'resolution', 'release_group']),
frozenset(['imdb_id', 'title', 'year']),
frozenset(['imdb_id', 'resolution', 'title', 'year'])}
with self.Provider() as provider:
subtitles = provider.query(languages, imdb_id=video.imdb_id)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_0_imdb_id(self):
video = EPISODES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['series', 'episode', 'season', 'imdb_id']),
frozenset(['episode', 'release_group', 'video_codec', 'series', 'imdb_id', 'resolution', 'season']),
frozenset(['series', 'imdb_id', 'video_codec', 'episode', 'season']),
frozenset(['episode', 'title', 'series', 'imdb_id', 'video_codec', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(languages, imdb_id=video.imdb_id)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_movie_0_hash(self):
video = MOVIES[0]
languages = {Language('eng')}
matches = {frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'imdb_id']),
frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'release_group', 'imdb_id']),
frozenset(['year', 'video_codec', 'imdb_id', 'hash', 'title']),
frozenset(['year', 'resolution', 'imdb_id', 'hash', 'title']),
frozenset(['year', 'imdb_id', 'hash', 'title'])}
with self.Provider() as provider:
subtitles = provider.query(languages, hash=video.hashes['opensubtitles'], size=video.size)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_0_hash(self):
video = EPISODES[0]
languages = {Language('eng')}
matches = {frozenset(['series', 'hash']),
frozenset(['episode', 'season', 'series', 'imdb_id', 'video_codec', 'hash']),
frozenset(['series', 'episode', 'season', 'hash', 'imdb_id']),
frozenset(['series', 'resolution', 'hash', 'video_codec'])}
with self.Provider() as provider:
subtitles = provider.query(languages, hash=video.hashes['opensubtitles'], size=video.size)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['title', 'video_codec', 'year', 'resolution', 'release_group', 'imdb_id']),
frozenset(['imdb_id', 'year', 'title']),
frozenset(['year', 'video_codec', 'imdb_id', 'resolution', 'title']),
frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'release_group', 'imdb_id']),
frozenset(['year', 'video_codec', 'imdb_id', 'hash', 'title']),
frozenset(['year', 'resolution', 'imdb_id', 'hash', 'title']),
frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'imdb_id']),
frozenset(['year', 'imdb_id', 'hash', 'title']),
frozenset(['video_codec', 'imdb_id', 'year', 'title']),
frozenset(['year', 'imdb_id', 'resolution', 'title'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class PodnapisiProviderTestCase(ProviderTestCase):
provider_name = 'podnapisi'
def test_query_movie_0(self):
video = MOVIES[0]
language = Language('eng')
matches = {frozenset(['video_codec', 'title', 'resolution', 'year']),
frozenset(['title', 'resolution', 'year']),
frozenset(['video_codec', 'title', 'year']),
frozenset(['title', 'year']),
frozenset(['video_codec', 'title', 'resolution', 'release_group', 'year'])}
with self.Provider() as provider:
subtitles = provider.query(language, title=video.title, year=video.year)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_query_episode_0(self):
video = EPISODES[0]
language = Language('eng')
matches = {frozenset(['episode', 'series', 'season', 'video_codec', 'resolution', 'release_group']),
frozenset(['season', 'video_codec', 'episode', 'resolution', 'series'])}
with self.Provider() as provider:
subtitles = provider.query(language, series=video.series, season=video.season, episode=video.episode)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_list_subtitles(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['video_codec', 'title', 'resolution', 'year']),
frozenset(['title', 'resolution', 'year']),
frozenset(['video_codec', 'title', 'year']),
frozenset(['title', 'year']),
frozenset(['video_codec', 'title', 'resolution', 'release_group', 'year'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class TheSubDBProviderTestCase(ProviderTestCase):
provider_name = 'thesubdb'
def test_query_episode_0(self):
video = EPISODES[0]
languages = {Language('eng'), Language('spa'), Language('por')}
matches = {frozenset(['hash'])}
with self.Provider() as provider:
subtitles = provider.query(video.hashes['thesubdb'])
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1(self):
video = EPISODES[1]
languages = {Language('eng'), Language('por')}
matches = {frozenset(['hash'])}
with self.Provider() as provider:
subtitles = provider.query(video.hashes['thesubdb'])
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = MOVIES[0]
languages = {Language('eng'), Language('por')}
matches = {frozenset(['hash'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = MOVIES[0]
languages = {Language('eng'), Language('por')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class TVsubtitlesProviderTestCase(ProviderTestCase):
provider_name = 'tvsubtitles'
def test_find_show_id(self):
with self.Provider() as provider:
show_id = provider.find_show_id('The Big Bang')
self.assertTrue(show_id == 154)
def test_find_show_id_ambiguous(self):
with self.Provider() as provider:
show_id = provider.find_show_id('New Girl')
self.assertTrue(show_id == 977)
def test_find_show_id_no_dots(self):
with self.Provider() as provider:
show_id = provider.find_show_id('Marvel\'s Agents of S H I E L D')
self.assertTrue(show_id == 1340)
def test_find_show_id_error(self):
with self.Provider() as provider:
show_id = provider.find_show_id('the big gaming')
self.assertTrue(show_id is None)
def test_find_episode_ids(self):
with self.Provider() as provider:
episode_ids = provider.find_episode_ids(154, 5)
self.assertTrue(set(episode_ids.keys()) == set(range(1, 25)))
def test_query_episode_0(self):
video = EPISODES[0]
languages = {Language('fra'), Language('por'), Language('hun'), Language('ron'), Language('eng')}
matches = {frozenset(['series', 'episode', 'season', 'video_codec']),
frozenset(['series', 'episode', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season, video.episode)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1(self):
video = EPISODES[1]
languages = {Language('fra'), Language('ell'), Language('ron'), Language('eng'), Language('hun'),
Language('por'), Language('por', 'BR')}
matches = {frozenset(['series', 'episode', 'resolution', 'season']),
frozenset(['series', 'episode', 'season', 'video_codec']),
frozenset(['series', 'episode', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season, video.episode)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = EPISODES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['series', 'episode', 'season'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = EPISODES[0]
languages = {Language('hun')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
def suite():
suite = TestSuite()
suite.addTest(TestLoader().loadTestsFromTestCase(Addic7edProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(BierDopjeProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(OpenSubtitlesProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(PodnapisiProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(TheSubDBProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(TVsubtitlesProviderTestCase))
return suite
if __name__ == '__main__':
TextTestRunner().run(suite())
-172
View File
@@ -1,172 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import shutil
from unittest import TestCase, TestSuite, TestLoader, TextTestRunner
from babelfish import Language
from subliminal import list_subtitles, download_subtitles, download_best_subtitles, scan_video
from subliminal.tests.common import MOVIES, EPISODES
TEST_DIR = 'test_data'
class ApiTestCase(TestCase):
def setUp(self):
os.mkdir(TEST_DIR)
def tearDown(self):
shutil.rmtree(TEST_DIR)
def test_list_subtitles_movie_0(self):
videos = [MOVIES[0]]
languages = {Language('eng')}
subtitles = list_subtitles(videos, languages)
self.assertTrue(len(subtitles) == len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_list_subtitles_movie_0_por_br(self):
videos = [MOVIES[0]]
languages = {Language('por', 'BR')}
subtitles = list_subtitles(videos, languages)
self.assertTrue(len(subtitles) == len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_list_subtitles_episodes(self):
videos = [EPISODES[0], EPISODES[1]]
languages = {Language('eng'), Language('fra')}
subtitles = list_subtitles(videos, languages)
self.assertTrue(len(subtitles) == len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_download_subtitles(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = list_subtitles(videos, languages)
download_subtitles(subtitles)
for video in videos:
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.en.srt'))
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.fr.srt'))
def test_download_subtitles_single(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = list_subtitles(videos, languages)
download_subtitles(subtitles, single=True)
for video in videos:
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.srt'))
def test_download_best_subtitles(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = download_best_subtitles(videos, languages)
for video in videos:
self.assertTrue(video in subtitles and len(subtitles[video]) == 2)
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.en.srt'))
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.fr.srt'))
def test_download_best_subtitles_single(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = download_best_subtitles(videos, languages, single=True)
for video in videos:
self.assertTrue(video in subtitles and len(subtitles[video]) == 1)
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.srt'))
def test_download_best_subtitles_min_score(self):
videos = [MOVIES[0]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = download_best_subtitles(videos, languages, min_score=1000)
self.assertTrue(len(subtitles) == 0)
def test_download_best_subtitles_hearing_impaired(self):
videos = [MOVIES[0]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng')}
subtitles = download_best_subtitles(videos, languages, hearing_impaired=True)
self.assertTrue(subtitles[videos[0]][0].hearing_impaired == True)
class VideoTestCase(TestCase):
def setUp(self):
os.mkdir(TEST_DIR)
for video in MOVIES + EPISODES:
open(os.path.join(TEST_DIR, os.path.split(video.name)[1]), 'w').close()
def tearDown(self):
shutil.rmtree(TEST_DIR)
def test_scan_video_movie(self):
video = MOVIES[0]
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.name == os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.title.lower() == video.title.lower())
self.assertTrue(scanned_video.year == video.year)
self.assertTrue(scanned_video.video_codec == video.video_codec)
self.assertTrue(scanned_video.resolution == video.resolution)
self.assertTrue(scanned_video.release_group == video.release_group)
self.assertTrue(scanned_video.subtitle_languages == set())
self.assertTrue(scanned_video.hashes == {})
self.assertTrue(scanned_video.audio_codec is None)
self.assertTrue(scanned_video.imdb_id is None)
self.assertTrue(scanned_video.size == 0)
def test_scan_video_episode(self):
video = EPISODES[0]
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.name == os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.series == video.series)
self.assertTrue(scanned_video.season == video.season)
self.assertTrue(scanned_video.episode == video.episode)
self.assertTrue(scanned_video.video_codec == video.video_codec)
self.assertTrue(scanned_video.resolution == video.resolution)
self.assertTrue(scanned_video.release_group == video.release_group)
self.assertTrue(scanned_video.subtitle_languages == set())
self.assertTrue(scanned_video.hashes == {})
self.assertTrue(scanned_video.title is None)
self.assertTrue(scanned_video.tvdb_id is None)
self.assertTrue(scanned_video.imdb_id is None)
self.assertTrue(scanned_video.audio_codec is None)
self.assertTrue(scanned_video.size == 0)
def test_scan_video_subtitle_language_und(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.subtitle_languages == {Language('und')})
def test_scan_video_subtitles_language_eng(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.en.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.subtitle_languages == {Language('eng')})
def test_scan_video_subtitles_languages(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.en.srt', 'w').close()
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.fr.srt', 'w').close()
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.subtitle_languages == {Language('eng'), Language('fra'), Language('und')})
def suite():
suite = TestSuite()
suite.addTest(TestLoader().loadTestsFromTestCase(ApiTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(VideoTestCase))
return suite
if __name__ == '__main__':
TextTestRunner().run(suite())
+152
View File
@@ -0,0 +1,152 @@
# -*- coding: utf-8 -*-
from datetime import datetime
import hashlib
import os
import re
import struct
def hash_opensubtitles(video_path):
"""Compute a hash using OpenSubtitles' algorithm.
:param str video_path: path of the video.
:return: the hash.
:rtype: str
"""
bytesize = struct.calcsize(b'<q')
with open(video_path, 'rb') as f:
filesize = os.path.getsize(video_path)
filehash = filesize
if filesize < 65536 * 2:
return
for _ in range(65536 // bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(b'<q', filebuffer)
filehash += l_value
filehash &= 0xFFFFFFFFFFFFFFFF # to remain as 64bit number
f.seek(max(0, filesize - 65536), 0)
for _ in range(65536 // bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(b'<q', filebuffer)
filehash += l_value
filehash &= 0xFFFFFFFFFFFFFFFF
returnedhash = '%016x' % filehash
return returnedhash
def hash_thesubdb(video_path):
"""Compute a hash using TheSubDB's algorithm.
:param str video_path: path of the video.
:return: the hash.
:rtype: str
"""
readsize = 64 * 1024
if os.path.getsize(video_path) < readsize:
return
with open(video_path, 'rb') as f:
data = f.read(readsize)
f.seek(-readsize, os.SEEK_END)
data += f.read(readsize)
return hashlib.md5(data).hexdigest()
def hash_napiprojekt(video_path):
"""Compute a hash using NapiProjekt's algorithm.
:param str video_path: path of the video.
:return: the hash.
:rtype: str
"""
readsize = 1024 * 1024 * 10
with open(video_path, 'rb') as f:
data = f.read(readsize)
return hashlib.md5(data).hexdigest()
def hash_shooter(video_path):
"""Compute a hash using Shooter's algorithm
:param string video_path: path of the video
:return: the hash
:rtype: string
"""
filesize = os.path.getsize(video_path)
readsize = 4096
if os.path.getsize(video_path) < readsize * 2:
return None
offsets = (readsize, filesize // 3 * 2, filesize // 3, filesize - readsize * 2)
filehash = []
with open(video_path, 'rb') as f:
for offset in offsets:
f.seek(offset)
filehash.append(hashlib.md5(f.read(readsize)).hexdigest())
return ';'.join(filehash)
def sanitize(string, ignore_characters=None):
"""Sanitize a string to strip special characters.
:param str string: the string to sanitize.
:param set ignore_characters: characters to ignore.
:return: the sanitized string.
:rtype: str
"""
# only deal with strings
if string is None:
return
ignore_characters = ignore_characters or set()
# replace some characters with one space
characters = {'-', ':', '(', ')', '.'} - ignore_characters
if characters:
string = re.sub(r'[%s]' % re.escape(''.join(characters)), ' ', string)
# remove some characters
characters = {'\''} - ignore_characters
if characters:
string = re.sub(r'[%s]' % re.escape(''.join(characters)), '', string)
# replace multiple spaces with one
string = re.sub(r'\s+', ' ', string)
# strip and lower case
return string.strip().lower()
def sanitize_release_group(string):
"""Sanitize a `release_group` string to remove content in square brackets.
:param str string: the release group to sanitize.
:return: the sanitized release group.
:rtype: str
"""
# only deal with strings
if string is None:
return
# remove content in square brackets
string = re.sub(r'\[\w+\]', '', string)
# strip and lower case
return string.strip().lower()
def timestamp(date):
"""Get the timestamp of the `date`, python2/3 compatible
:param datetime.datetime date: the utc date.
:return: the timestamp of the date.
:rtype: float
"""
return (date - datetime(1970, 1, 1)).total_seconds()
+135 -285
View File
@@ -1,14 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import datetime
import hashlib
from __future__ import division
from datetime import datetime, timedelta
import logging
import os
import struct
import babelfish
import enzyme
import guessit
from guessit import guessit
logger = logging.getLogger(__name__)
@@ -21,50 +17,95 @@ VIDEO_EXTENSIONS = ('.3g2', '.3gp', '.3gp2', '.3gpp', '.60d', '.ajp', '.asf', '.
'.qt', '.ram', '.rm', '.rmvb', '.swf', '.ts', '.vfw', '.vid', '.video', '.viv', '.vivo', '.vob',
'.vro', '.wm', '.wmv', '.wmx', '.wrap', '.wvx', '.wx', '.x264', '.xvid')
#: Subtitle extensions
SUBTITLE_EXTENSIONS = ('.srt', '.sub', '.smi', '.txt', '.ssa', '.ass', '.mpl')
class Video(object):
"""Base class for videos
"""Base class for videos.
Represent a video, existing or not, with various properties that defines it.
Each property has an associated score based on equations that are described in
subclasses.
Represent a video, existing or not.
:param string name: name or path of the video
:param string release_group: release group of the video
:param string resolution: screen size of the video stream (480p, 720p, 1080p or 1080i)
:param string video_codec: codec of the video stream
:param string audio_codec: codec of the main audio stream
:param int imdb_id: IMDb id of the video
:param dict hashes: hashes of the video file by provider names
:param int size: byte size of the video file
:param set subtitle_languages: existing subtitle languages
:param str name: name or path of the video.
:param str format: format of the video (HDTV, WEB-DL, BluRay, ...).
:param str release_group: release group of the video.
:param str resolution: resolution of the video stream (480p, 720p, 1080p or 1080i).
:param str video_codec: codec of the video stream.
:param str audio_codec: codec of the main audio stream.
:param str imdb_id: IMDb id of the video.
:param dict hashes: hashes of the video file by provider names.
:param int size: size of the video file in bytes.
:param set subtitle_languages: existing subtitle languages.
"""
scores = {}
def __init__(self, name, release_group=None, resolution=None, video_codec=None, audio_codec=None, imdb_id=None,
hashes=None, size=None, subtitle_languages=None):
def __init__(self, name, format=None, release_group=None, resolution=None, video_codec=None, audio_codec=None,
imdb_id=None, hashes=None, size=None, subtitle_languages=None):
#: Name or path of the video
self.name = name
#: Format of the video (HDTV, WEB-DL, BluRay, ...)
self.format = format
#: Release group of the video
self.release_group = release_group
#: Resolution of the video stream (480p, 720p, 1080p or 1080i)
self.resolution = resolution
#: Codec of the video stream
self.video_codec = video_codec
#: Codec of the main audio stream
self.audio_codec = audio_codec
#: IMDb id of the video
self.imdb_id = imdb_id
#: Hashes of the video file by provider names
self.hashes = hashes or {}
#: Size of the video file in bytes
self.size = size
#: Existing subtitle languages
self.subtitle_languages = subtitle_languages or set()
@property
def exists(self):
"""Test whether the video exists"""
return os.path.exists(self.name)
@property
def age(self):
"""Age of the video"""
if self.exists:
return datetime.utcnow() - datetime.utcfromtimestamp(os.path.getmtime(self.name))
return timedelta()
@classmethod
def fromguess(cls, name, guess):
"""Create an :class:`Episode` or a :class:`Movie` with the given `name` based on the `guess`.
:param str name: name of the video.
:param dict guess: guessed data.
:raise: :class:`ValueError` if the `type` of the `guess` is invalid
"""
if guess['type'] == 'episode':
return Episode.fromguess(name, guess)
if guess['type'] == 'movie':
return Movie.fromguess(name, guess)
raise ValueError('The guess must be an episode or a movie guess')
@classmethod
def fromname(cls, name):
"""Shortcut for :meth:`fromguess` with a `guess` guessed from the `name`.
:param str name: name of the video.
"""
return cls.fromguess(name, guessit(name))
def __repr__(self):
return '<%s [%r]>' % (self.__class__.__name__, self.name)
@@ -73,299 +114,108 @@ class Video(object):
class Episode(Video):
"""Episode :class:`Video`
"""Episode :class:`Video`.
Scores are defined by a set of equations, see :func:`~subliminal.score.get_episode_equations`
:param string series: series of the episode
:param int season: season number of the episode
:param int episode: episode number of the episode
:param string title: title of the episode
:param int tvdb_id: TheTVDB id of the episode
:param str series: series of the episode.
:param int season: season number of the episode.
:param int episode: episode number of the episode.
:param str title: title of the episode.
:param int year: year of the series.
:param bool original_series: whether the series is the first with this name.
:param int tvdb_id: TVDB id of the episode.
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
"""
scores = {'title': 12, 'video_codec': 2, 'imdb_id': 35, 'audio_codec': 1, 'tvdb_id': 23, 'resolution': 2,
'season': 6, 'release_group': 6, 'series': 23, 'episode': 6, 'hash': 46}
def __init__(self, name, series, season, episode, title=None, year=None, original_series=True, tvdb_id=None,
series_tvdb_id=None, series_imdb_id=None, **kwargs):
super(Episode, self).__init__(name, **kwargs)
def __init__(self, name, series, season, episode, release_group=None, resolution=None, video_codec=None,
audio_codec=None, imdb_id=None, hashes=None, size=None, subtitle_languages=None, title=None,
tvdb_id=None):
super(Episode, self).__init__(name, release_group, resolution, video_codec, audio_codec, imdb_id, hashes,
size, subtitle_languages)
#: Series of the episode
self.series = series
#: Season number of the episode
self.season = season
#: Episode number of the episode
self.episode = episode
#: Title of the episode
self.title = title
#: Year of series
self.year = year
#: The series is the first with this name
self.original_series = original_series
#: TVDB id of the episode
self.tvdb_id = tvdb_id
#: TVDB id of the series
self.series_tvdb_id = series_tvdb_id
#: IMDb id of the series
self.series_imdb_id = series_imdb_id
@classmethod
def fromguess(cls, name, guess):
if guess['type'] != 'episode':
raise ValueError('The guess must be an episode guess')
if 'series' not in guess or 'season' not in guess or 'episodeNumber' not in guess:
if 'title' not in guess or 'episode' not in guess:
raise ValueError('Insufficient data to process the guess')
return cls(name, guess['series'], guess['season'], guess['episodeNumber'],
release_group=guess.get('releaseGroup'), resolution=guess.get('screenSize'),
video_codec=guess.get('videoCodec'), audio_codec=guess.get('audioCodec'),
title=guess.get('title'))
return cls(name, guess['title'], guess.get('season', 1), guess['episode'], title=guess.get('episode_title'),
year=guess.get('year'), format=guess.get('format'), original_series='year' not in guess,
release_group=guess.get('release_group'), resolution=guess.get('screen_size'),
video_codec=guess.get('video_codec'), audio_codec=guess.get('audio_codec'))
@classmethod
def fromname(cls, name):
return cls.fromguess(name, guessit(name, {'type': 'episode'}))
def __repr__(self):
return '<%s [%r, %rx%r]>' % (self.__class__.__name__, self.series, self.season, self.episode)
if self.year is None:
return '<%s [%r, %dx%d]>' % (self.__class__.__name__, self.series, self.season, self.episode)
return '<%s [%r, %d, %dx%d]>' % (self.__class__.__name__, self.series, self.year, self.season, self.episode)
class Movie(Video):
"""Movie :class:`Video`
"""Movie :class:`Video`.
Scores are defined by a set of equations, see :func:`~subliminal.score.get_movie_equations`
:param string title: title of the movie
:param int year: year of the movie
:param str title: title of the movie.
:param int year: year of the movie.
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
"""
scores = {'title': 13, 'video_codec': 2, 'resolution': 2, 'audio_codec': 1, 'year': 7, 'imdb_id': 31,
'release_group': 6, 'hash': 31}
def __init__(self, name, title, year=None, **kwargs):
super(Movie, self).__init__(name, **kwargs)
def __init__(self, name, title, release_group=None, resolution=None, video_codec=None, audio_codec=None,
imdb_id=None, hashes=None, size=None, subtitle_languages=None, year=None):
super(Movie, self).__init__(name, release_group, resolution, video_codec, audio_codec, imdb_id, hashes,
size, subtitle_languages)
#: Title of the movie
self.title = title
#: Year of the movie
self.year = year
@classmethod
def fromguess(cls, name, guess):
if guess['type'] != 'movie':
raise ValueError('The guess must be a movie guess')
if 'title' not in guess:
raise ValueError('Insufficient data to process the guess')
return cls(name, guess['title'], release_group=guess.get('releaseGroup'), resolution=guess.get('screenSize'),
video_codec=guess.get('videoCodec'), audio_codec=guess.get('audioCodec'),
year=guess.get('year'))
return cls(name, guess['title'], format=guess.get('format'), release_group=guess.get('release_group'),
resolution=guess.get('screen_size'), video_codec=guess.get('video_codec'),
audio_codec=guess.get('audio_codec'), year=guess.get('year'))
@classmethod
def fromname(cls, name):
return cls.fromguess(name, guessit(name, {'type': 'movie'}))
def __repr__(self):
if self.year is None:
return '<%s [%r]>' % (self.__class__.__name__, self.title)
return '<%s [%r, %r]>' % (self.__class__.__name__, self.title, self.year)
def scan_subtitle_languages(path):
"""Search for subtitles with alpha2 extension from a video `path` and return their language
:param string path: path to the video
:return: found subtitle languages
:rtype: set
"""
language_extensions = tuple('.' + c for c in babelfish.CONVERTERS['alpha2'].codes)
dirpath, filename = os.path.split(path)
subtitles = set()
for p in os.listdir(dirpath):
if not isinstance(p, bytes) and p.startswith(os.path.splitext(filename)[0]) and p.endswith(SUBTITLE_EXTENSIONS):
if os.path.splitext(p)[0].endswith(language_extensions):
subtitles.add(babelfish.Language.fromalpha2(os.path.splitext(p)[0][-2:]))
else:
subtitles.add(babelfish.Language('und'))
logger.debug('Found subtitles %r', subtitles)
return subtitles
def scan_video(path, subtitles=True, embedded_subtitles=True):
"""Scan a video and its subtitle languages from a video `path`
:param string path: absolute path to the video
:param bool subtitles: scan for subtitles with the same name
:param bool embedded_subtitles: scan for embedded subtitles
:return: the scanned video
:rtype: :class:`Video`
:raise: ValueError if cannot guess enough information from the path
"""
dirpath, filename = os.path.split(path)
logger.info('Scanning video %r in %r', filename, dirpath)
video = Video.fromguess(path, guessit.guess_file_info(path, 'autodetect'))
video.size = os.path.getsize(path)
if video.size > 10485760:
logger.debug('Size is %d', video.size)
video.hashes['opensubtitles'] = hash_opensubtitles(path)
video.hashes['thesubdb'] = hash_thesubdb(path)
logger.debug('Computed hashes %r', video.hashes)
else:
logger.warning('Size is lower than 10MB: hashes not computed')
if subtitles:
video.subtitle_languages |= scan_subtitle_languages(path)
# enzyme
try:
if filename.endswith('.mkv'):
with open(path, 'rb') as f:
mkv = enzyme.MKV(f)
if mkv.video_tracks:
video_track = mkv.video_tracks[0]
# resolution
if video_track.height in (480, 720, 1080):
if video_track.interlaced:
video.resolution = '%di' % video_track.height
logger.debug('Found resolution %s with enzyme', video.resolution)
else:
video.resolution = '%dp' % video_track.height
logger.debug('Found resolution %s with enzyme', video.resolution)
# video codec
if video_track.codec_id == 'V_MPEG4/ISO/AVC':
video.video_codec = 'h264'
logger.debug('Found video_codec %s with enzyme', video.video_codec)
elif video_track.codec_id == 'V_MPEG4/ISO/SP':
video.video_codec = 'DivX'
logger.debug('Found video_codec %s with enzyme', video.video_codec)
elif video_track.codec_id == 'V_MPEG4/ISO/ASP':
video.video_codec = 'XviD'
logger.debug('Found video_codec %s with enzyme', video.video_codec)
else:
logger.warning('MKV has no video track')
if mkv.audio_tracks:
audio_track = mkv.audio_tracks[0]
# audio codec
if audio_track.codec_id == 'A_AC3':
video.audio_codec = 'AC3'
logger.debug('Found audio_codec %s with enzyme', video.audio_codec)
elif audio_track.codec_id == 'A_DTS':
video.audio_codec = 'DTS'
logger.debug('Found audio_codec %s with enzyme', video.audio_codec)
elif audio_track.codec_id == 'A_AAC':
video.audio_codec = 'AAC'
logger.debug('Found audio_codec %s with enzyme', video.audio_codec)
else:
logger.warning('MKV has no audio track')
if mkv.subtitle_tracks:
# embedded subtitles
if embedded_subtitles:
embedded_subtitle_languages = set()
for st in mkv.subtitle_tracks:
try:
embedded_subtitle_languages.add(babelfish.Language.fromalpha3b(st.language or 'und'))
except babelfish.Error:
logger.error('Embedded subtitle language %r is not a valid language', st.language)
logger.debug('Found embedded subtitle %r with enzyme', embedded_subtitle_languages)
video.subtitle_languages |= embedded_subtitle_languages
else:
logger.debug('MKV has no subtitle track')
except enzyme.Error:
logger.error('Parsing video metadata with enzyme failed')
return video
def scan_videos(paths, subtitles=True, embedded_subtitles=True, age=None):
"""Scan `paths` for videos and their subtitle languages
:params paths: absolute paths to scan for videos
:type paths: list of string
:param bool subtitles: scan for subtitles with the same name
:param bool embedded_subtitles: scan for embedded subtitles
:param age: age of the video, if any
:type age: datetime.timedelta or None
:return: the scanned videos
:rtype: list of :class:`Video`
"""
videos = []
# scan files
for filepath in [p for p in paths if os.path.isfile(p)]:
if age and datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(filepath)) > age:
logger.info('Skipping video %r: older than %r', filepath, age)
continue
try:
videos.append(scan_video(filepath, subtitles, embedded_subtitles))
except ValueError as e:
logger.error('Skipping video: %s', e)
continue
# scan directories
for path in [p for p in paths if os.path.isdir(p)]:
logger.info('Scanning directory %r', path)
for dirpath, dirnames, filenames in os.walk(path):
# skip badly encoded directories
if isinstance(dirpath, bytes):
logger.error('Skipping badly encoded directory %r', dirpath.decode('utf-8', errors='replace'))
continue
# skip badly encoded and hidden sub directories
for dirname in list(dirnames):
if isinstance(dirname, bytes):
logger.error('Skipping badly encoded dirname %r in %r', dirname.decode('utf-8', errors='replace'),
dirpath)
dirnames.remove(dirname)
elif dirname.startswith('.'):
logger.debug('Skipping hidden dirname %r in %r', dirname, dirpath)
dirnames.remove(dirname)
# scan for videos
for filename in filenames:
# skip badly encoded files
if isinstance(filename, bytes):
logger.error('Skipping badly encoded filename %r in %r', filename.decode('utf-8', errors='replace'),
dirpath)
continue
# filter videos
if not filename.endswith(VIDEO_EXTENSIONS):
continue
# skip hidden files
if filename.startswith('.'):
logger.debug('Skipping hidden filename %r in %r', filename, dirpath)
continue
filepath = os.path.join(dirpath, filename)
# skip links
if os.path.islink(filepath):
logger.debug('Skipping link %r in %r', filename, dirpath)
continue
if age and datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(filepath)) > age:
logger.info('Skipping video %r: older than %r', filepath, age)
continue
try:
video = scan_video(filepath, subtitles, embedded_subtitles)
except ValueError as e:
logger.error('Skipping video: %s', e)
continue
videos.append(video)
return videos
def hash_opensubtitles(video_path):
"""Compute a hash using OpenSubtitles' algorithm
:param string video_path: path of the video
:return: the hash
:rtype: string
"""
bytesize = struct.calcsize(b'q')
with open(video_path, 'rb') as f:
filesize = os.path.getsize(video_path)
filehash = filesize
if filesize < 65536 * 2:
return None
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(b'q', filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF # to remain as 64bit number
f.seek(max(0, filesize - 65536), 0)
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(b'q', filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF
returnedhash = '%016x' % filehash
return returnedhash
def hash_thesubdb(video_path):
"""Compute a hash using TheSubDB's algorithm
:param string video_path: path of the video
:return: the hash
:rtype: string
"""
readsize = 64 * 1024
if os.path.getsize(video_path) < readsize:
return None
with open(video_path, 'rb') as f:
data = f.read(readsize)
f.seek(-readsize, os.SEEK_END)
data += f.read(readsize)
return hashlib.md5(data).hexdigest().decode('ascii')
return '<%s [%r, %d]>' % (self.__class__.__name__, self.title, self.year)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+240
View File
@@ -0,0 +1,240 @@
interactions:
- request:
body: username=subliminal&password=subliminal&Submit=Log+in
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
Content-Length: ['53']
Content-Type: [application/x-www-form-urlencoded]
User-Agent: [Subliminal/2.0]
method: POST
uri: http://www.addic7ed.com/dologin.php
response:
body: {string: !!python/unicode "\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0\
\ Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"\
>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta http-equiv=\"\
Content-Type\" content=\"text/html; charset=utf-8\" />\n<title>Addic7ed.com\
\ - For all those TV Series Addic7s: Subtitles, Tv Series and Movies Talk,\
\ Forum and more -</title>\n<link href=\"css/wikisubtitles.css\" rel=\"stylesheet\"\
\ title=\"default\" type=\"text/css\" media=\"screen\"/>\n</head>\n\n<body>\n\
<center><br />\n<table border=\"0\">\n<tr>\n <td rowspan=\"9\"><a href=\"\
/\"><img height=\"200\" width=\"350\" src=\"http://www.addic7ed.com/images/addic7edheader.jpg\"\
\ border=\"0\" title=\"Addic7ed.com - Quality Subtitles for TV Shows and\
\ movies\" alt=\"Addic7ed.com - Quality Subtitles for TV Shows and movies\"\
\ /></a></td>\n</tr>\n<tr><td align=\"center\" colspan=\"2\">\n<h1><small>Download\
\ free subtitles for TV Shows and Movies.</small>&nbsp; \n<select name=\"\
applang\" class=\"inputCool\" onchange=\"changeAppLang();\" id=\"comboLang\"\
><option value=\"ar\">Arabic</option><option value=\"ca\">Catala</option><option\
\ selected=\"selected\" value=\"en\">English</option><option value=\"eu\"\
>Euskera</option><option value=\"fr\">French</option><option value=\"ga\"\
>Galician</option><option value=\"de\">German</option><option value=\"gr\"\
>Greek</option><option value=\"hu\">Hungarian</option><option value=\"it\"\
>Italian</option><option value=\"fa\">Persian</option><option value=\"pl\"\
>Polish</option><option value=\"pt\">Portuguese</option><option value=\"br\"\
>Portuguese (Brazilian)</option><option value=\"ro\">Romanian</option><option\
\ value=\"ru\">Russian</option><option value=\"es\">Spanish</option><option\
\ value=\"se\">Swedish</option></select></h1>\n</td></tr>\n<tr><td align=\"\
center\" colspan=\"2\">\n\n<script language=\"javascript\">\nvar url=\"/msgspopup.php?count=1\"\
;\t\teditwin = window.open(url, \"msgswin\", 'height=200,width=350,toolbar=0,location=0,statusbar=0,menubar=0');\
\ \n\t\tif (editwin.focus) {editwin.focus()}\n</script>\n<div id=\"hBar\"\
>\n\t\t\t <ul>\n\t\t\t\t<li><a class=\"button white\" href=\"/panel.php\"\
>My Panel</a></li><li><a class=\"button white\" href=\"/newsub.php\">Upload</a></li>\t\
\t\t<li><a class=\"button white\" href=\"/shows.php\">Shows</a></li>\n\t\t\
\t\t<li><a class=\"button white\" href=\"/allshows/a\">Air dates</a></li>\n\
\t\t\t\t<li><a class=\"button white\" href=\"http://www.sub-talk.net\">Forum</a></li>\n\
\t\t\t\t<li><a class=\"button white\" href=\"/logout.php\">Logout</a></li>\n\
\t\t\t </ul>\n\t\t\t </div>\n</td></tr> \n<tr>\n <td>\n</td><td>\n\t<g:plusone\
\ size=\"medium\"></g:plusone>\n <a href=\"http://twitter.com/addic7ed\"\
\ target=\"_blank\"><img width=\"32\" height=\"32\" src=\"http://www.addic7ed.com/images/twitter_right.png\"\
\ alt=\"Twitter\" border=\"0\" /></a>\n\t<a href=\"irc://irc.efnet.net:6667/addic7ed\"\
><img width=\"32\" height=\"32\" src=\"http://www.addic7ed.com/images/irc-right.png\"\
\ alt=\"IRC\" border=\"0\" /></a>\n<div style=\"float: right; padding-right:10%;\"\
>\n\n </td>\n </tr>\n <tr>\n <td colspan=2><iframe src=\"http://www.facebook.com/plugins/like.php?href=https%3A%2F%2Fwww.facebook.com%2FAddic7ed&amp;send=false&amp;layout=button_count&amp;width=450&amp;show_faces=false&amp;action=like&amp;colorscheme=light&amp;font=tahoma&amp;height=21&amp;appId=121322186712\"\
\ scrolling=\"no\" frameborder=\"0\" style=\"border:none; overflow:hidden;\
\ width:80px; height:21px;\" allowTransparency=\"true\"></iframe>\n </td>\n\
\ </tr>\n</table>\n</center>\n\n<center>\n\n<!--[if lt IE 7]>\n <style type=\"\
text/css\">\n div, img { behavior: url(http://www.addic7ed.com/js/iepngfix.htc)\
\ }\n </style>\n<![endif]-->\n\n<center><table border=\"0\" width=\"90%\"\
>\n<tr>\n<td class=\"NewsTitle\"><img width=\"20\" height=\"20\" src=\"http://www.addic7ed.com/images/television.png\"\
\ alt=\"TV\" /><img src=\"http://www.addic7ed.com/images/invisible.gif\" alt=\"\
\ \" />Addic7ed</td>\n<td class=\"NewsTitle\"><img width=\"20\" height=\"\
20\" src=\"http://www.addic7ed.com/images/television.png\" alt=\"TV\" /><img\
\ src=\"http://www.addic7ed.com/images/invisible.gif\" alt=\" \" />Popular\
\ Shows</td>\n<td class=\"NewsTitle\"><img width=\"20\" height=\"20\" src=\"\
http://www.addic7ed.com/images/television.png\" alt=\"TV\" /><img src=\"http://www.addic7ed.com/images/invisible.gif\"\
\ alt=\" \" />Useful</td>\n<td class=\"NewsTitle\"><img width=\"20\" height=\"\
20\" src=\"http://www.addic7ed.com/images/television.png\" alt=\"TV\" /><img\
\ src=\"http://www.addic7ed.com/images/invisible.gif\" alt=\" \" />Forums</td>\n\
</tr>\n<tr>\n<td><div id=\"footermenu\"><a href=\"/shows.php\">Browse By Shows</a></div></td>\n\
<td><div id=\"footermenu\"><a href=\"/show/4906\">12 Monkeys</a></div></td>\n\
<td><div id=\"footermenu\"><a href=\"/shows-schedule\">TV Shows Schedule</a></div></td>\n\
<td><div id=\"footermenu\"><a href=\"http://www.sub-talk.net/topic/1031-changelog/\"\
>Site Changelog</a></div></td>\n</tr>\n<tr>\n<td><div id=\"footermenu\"><a\
\ href=\"/movie-subtitles\">Browse By Movies</a></div></td>\n<td><div id=\"\
footermenu\"><a href=\"/show/1812\">Homeland</a></div></td>\n<td><div id=\"\
footermenu\"><a href=\"http://www.sub-talk.net/topic/2784-frequently-asked-questions/\"\
>Frequently Asked Questions</a></div></td>\n<td><div id=\"footermenu\">Support\
\ Us</div></td>\n</tr>\n<tr>\n<td><div id=\"footermenu\"><a href=\"/top-uploaders\"\
>Top Uploaders</a></div></td>\n<td><div id=\"footermenu\"><a href=\"/show/620\"\
>Modern Family</a></div></td>\n<td><div id=\"footermenu\">RSS Feeds</div></td>\n\
<td><div id=\"footermenu\">Premium Accounts</div></td>\n</tr>\n<tr>\n<td><div\
\ id=\"footermenu\"><a href=\"/log.php?mode=downloaded\">Top Downloads</a></div></td>\n\
<td><div id=\"footermenu\"><a href=\"/show/466\">Glee</a></div></td>\n<td\
\ class=\"NewsTitle\"><img width=\"20\" height=\"20\" src=\"http://www.addic7ed.com/images/television.png\"\
\ alt=\"TV\" /><img src=\"http://www.addic7ed.com/images/invisible.gif\" alt=\"\
\ \"/>Tutorials</td>\n<td><div id=\"footermenu\"><a href=\"http://sub-talk.net/thread-6-1-1.html\"\
>Video Formats</a></div></td>\n</tr>\n<tr>\n<td><div id=\"footermenu\"><a\
\ href=\"/log.php?mode=news\">All News</a></div></td>\n<td><div id=\"footermenu\"\
><a href=\"/show/450\">Parks and Recreation</a></div></td>\n<td><div id=\"\
footermenu\"><a href=\"http://www.sub-talk.net/topic/338-guide-to-syncing-with-subtitleedit/page__p__1485__hl__%2B+%2Bsync__fromsearch__1#entry1485\"\
>How to Synchronize Subtitles</a></div></td>\n<td><div id=\"footermenu\">Frequently\
\ Asked Questions</div></td>\n</tr> \n<tr>\n<td><div id=\"footermenu\"><a\
\ href=\"http://www.sub-talk.net\">Sub-Talk Forums</a></div></td>\n<td><div\
\ id=\"footermenu\"><a href=\"/show/1277\">Shameless (US)</a></div></td>\n\
<td><div id=\"footermenu\">What Are Subtitles</div></td>\n<td><div id=\"footermenu\"\
><a href=\"http://sub-talk.net/index.php?gid=7\">TV Shows Talk</a></div></td>\n\
</tr>\n<tr>\n<td><div id=\"footermenu\"><a href=\"/latest_comments.php\">Latest\
\ Comments</a></div></td>\n<td><div id=\"footermenu\"><a href=\"/show/126\"\
>The Big Bang Theory</a></div></td>\n<td><div id=\"footermenu\">New Translation\
\ Tutorial</div></td>\n<td><div id=\"footermenu\"><a href=\"http://sub-talk.net/index.php?gid=22\"\
>Movies Talk</a></div></td>\n</tr>\n<tr>\n<td><div id=\"footermenu\"><a href=\"\
http://www.vreaubagaj.ro/troler/\" title=\"Trolere ieftine\" alt=\"Trolere\
\ ieftine\">Trolere ieftine</a></div></td>\n<td><div id=\"footermenu\"><a\
\ href=\"/show/130\">Family Guy</a></div></td>\n<td><div id=\"footermenu\"\
>Upload a New Subtitle Tutorial</div></td>\n<td class=\"NewsTitle\"><img width=\"\
20\" height=\"20\" src=\"http://www.addic7ed.com/images/television.png\" alt=\"\
TV\" /><img src=\"http://www.addic7ed.com/images/invisible.gif\" alt=\" \"\
\ />Stats</td>\n</tr>\n<tr>\n<td><div id=\"footermenu\">Terms of Service</div></td>\n\
<td><div id=\"footermenu\"><a href=\"/show/1799\">American Horror Story</a></div></td>\n\
<td><div id=\"footermenu\"><a href=\"http://sub-talk.net/viewthread.php?tid=294\"\
>How to have an Avatar</a></div></td>\n<td align=\"left\">.\n\t\t\t\t</td>\n\
</tr>\n<tr>\n<td><div id=\"footermenu\"><a href=\"/contact.php\">Contact</a></div></td>\n\
<td><div id=\"footermenu\"><a href=\"/show/15\">House</a></div></td>\n<td><div\
\ id=\"footermenu\"><a href=\"http://www.vreaubagaj.ro/\" alt=\"Trolere\"\
\ title=\"Trolere\">Trolere</a></div></td>\n<td>\n</td>\n</tr>\n</table></center>\n\
</center>\n\n<script type=\"text/javascript\">\nvar gaJsHost = ((\"https:\"\
\ == document.location.protocol) ? \"https://ssl.\" : \"http://www.\");\n\
document.write(unescape(\"%3Cscript src='\" + gaJsHost + \"google-analytics.com/ga.js'\
\ type='text/javascript'%3E%3C/script%3E\"));\n</script>\n<script type=\"\
text/javascript\">\ntry {\nvar pageTracker = _gat._getTracker(\"UA-10775680-1\"\
);\npageTracker._trackPageview();\n} catch(err) {}</script>\n\n\n<script type=\"\
text/javascript\" src=\"http://apis.google.com/js/plusone.js\"></script>\n\
\ \
\ \n</body>\n</html>\n"}
headers:
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
connection: [keep-alive]
content-type: [text/html]
date: ['Tue, 09 Feb 2016 15:57:53 GMT']
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
location: [/]
pragma: [no-cache]
server: [nginx]
set-cookie: [PHPSESSID=rk916i9m3r5gl5hmvs37l6m5p0; path=/]
x-powered-by: [PHP/5.3.3]
status: {code: 302, message: Moved Temporarily}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
Cookie: [PHPSESSID=rk916i9m3r5gl5hmvs37l6m5p0]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://www.addic7ed.com/panel.php
response:
body:
string: !!binary |
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
headers:
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
connection: [keep-alive]
content-encoding: [gzip]
content-type: [text/html]
date: ['Tue, 09 Feb 2016 15:57:53 GMT']
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
pragma: [no-cache]
server: [nginx]
x-powered-by: [PHP/5.3.3]
status: {code: 200, message: OK}
version: 1
@@ -0,0 +1,75 @@
interactions:
- request:
body: username=subliminal&password=lanimilbus&Submit=Log+in
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
Content-Length: ['53']
Content-Type: [application/x-www-form-urlencoded]
User-Agent: [Subliminal/2.0]
method: POST
uri: http://www.addic7ed.com/dologin.php
response:
body:
string: !!binary |
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
headers:
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
connection: [keep-alive]
content-encoding: [gzip]
content-type: [text/html]
date: ['Tue, 09 Feb 2016 15:57:53 GMT']
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
pragma: [no-cache]
server: [nginx]
set-cookie: [PHPSESSID=4ep7cufhhkte78hs23ijqj12u4; path=/]
x-powered-by: [PHP/5.3.3]
status: {code: 200, message: OK}
version: 1
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,228 @@
interactions:
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://www.addic7ed.com/search.php?search=The+Big+Bang+Theory&Submit=Search
response:
body:
string: !!binary |
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
headers:
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
connection: [keep-alive]
content-encoding: [gzip]
content-type: [text/html]
date: ['Tue, 09 Feb 2016 15:57:54 GMT']
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
pragma: [no-cache]
server: [nginx]
set-cookie: [PHPSESSID=vtudob3ke7abdfsgve42bidls4; path=/]
x-powered-by: [PHP/5.3.3]
status: {code: 200, message: OK}
version: 1
@@ -0,0 +1,97 @@
interactions:
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://www.addic7ed.com/search.php?search=The+Big+How+I+Met+Your+Mother&Submit=Search
response:
body:
string: !!binary |
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
//Y3/gsAd1cq8jQAAA==
headers:
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
connection: [keep-alive]
content-encoding: [gzip]
content-type: [text/html]
date: ['Tue, 09 Feb 2016 15:57:58 GMT']
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
pragma: [no-cache]
server: [nginx]
set-cookie: [PHPSESSID=nrofdh6anqhelau1gujne69cs4; path=/]
x-powered-by: [PHP/5.3.3]
status: {code: 200, message: OK}
version: 1
@@ -0,0 +1,228 @@
interactions:
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://www.addic7ed.com/search.php?search=The+Big+Bang&Submit=Search
response:
body:
string: !!binary |
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
headers:
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
connection: [keep-alive]
content-encoding: [gzip]
content-type: [text/html]
date: ['Tue, 09 Feb 2016 15:57:55 GMT']
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
pragma: [no-cache]
server: [nginx]
set-cookie: [PHPSESSID=5t7377c0qtq8esk06s11u5qvf5; path=/]
x-powered-by: [PHP/5.3.3]
status: {code: 200, message: OK}
version: 1
@@ -0,0 +1,205 @@
interactions:
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://www.addic7ed.com/search.php?search=Dallas&Submit=Search
response:
body:
string: !!binary |
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
headers:
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
connection: [keep-alive]
content-encoding: [gzip]
content-type: [text/html]
date: ['Tue, 09 Feb 2016 15:57:56 GMT']
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
pragma: [no-cache]
server: [nginx]
set-cookie: [PHPSESSID=0o8itpncimkt6g8g151qo9ded7; path=/]
x-powered-by: [PHP/5.3.3]
status: {code: 200, message: OK}
version: 1
@@ -0,0 +1,236 @@
interactions:
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://www.addic7ed.com/search.php?search=Grey+s+Anatomy&Submit=Search
response:
body:
string: !!binary |
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
headers:
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
connection: [keep-alive]
content-encoding: [gzip]
content-type: [text/html]
date: ['Tue, 09 Feb 2016 15:57:58 GMT']
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
pragma: [no-cache]
server: [nginx]
set-cookie: [PHPSESSID=knqqbsvm08522036a1kfq9oi22; path=/]
x-powered-by: [PHP/5.3.3]
status: {code: 200, message: OK}
version: 1
@@ -0,0 +1,121 @@
interactions:
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://www.addic7ed.com/search.php?search=Dallas+2012&Submit=Search
response:
body:
string: !!binary |
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
fpP1PWt3YKU8kBEHniH/7e79L7UagjE3VAAA
headers:
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
connection: [keep-alive]
content-encoding: [gzip]
content-type: [text/html]
date: ['Tue, 09 Feb 2016 15:57:57 GMT']
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
pragma: [no-cache]
server: [nginx]
set-cookie: [PHPSESSID=tdonebpl9qk72a0kr1fcarve56; path=/]
x-powered-by: [PHP/5.3.3]
status: {code: 200, message: OK}
version: 1
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More