Compare commits

...

425 Commits

Author SHA1 Message Date
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 e7f89c1a19 Release 0.7.2 2013-11-10 11:06:46 +01:00
Antoine Bertin be4f9d92eb Remove unused import 2013-11-10 11:03:37 +01:00
Antoine Bertin ca63b97e79 Parse IETF language format in cli 2013-11-10 10:25:00 +01:00
Antoine Bertin c1ed4a0232 Fix exception handling when validating subtitle 2013-11-10 10:24:23 +01:00
Antoine Bertin b826a0bf08 Use debug level for subtitle track detection 2013-11-10 10:23:41 +01:00
Antoine Bertin fd0d87d719 Use info level when skipping providers 2013-11-10 10:23:24 +01:00
Antoine Bertin 094373f3c1 Add podnapisi provider 2013-11-10 10:22:33 +01:00
Antoine Bertin 5dac623c9f Update to babelfish 0.3.0 2013-11-09 20:25:39 +01:00
Antoine Bertin 57d1e772ec Use more list comprehension 2013-11-09 18:09:19 +01:00
Antoine Bertin 983efbfd9b Reduce debug logging in opensubtitles 2013-11-09 18:06:52 +01:00
Antoine Bertin 1f11e293c1 Add missing docstring 2013-11-09 18:06:17 +01:00
Antoine Bertin bfd278ae1c Change Subtitle repr language format 2013-11-09 18:05:48 +01:00
Antoine Bertin dbe1b9d2af Update guessit requirement 2013-11-09 03:04:13 +01:00
Antoine Bertin d71bc4bf09 Set CLI default cache expiration time to 30 days 2013-11-07 00:33:38 +01:00
Antoine Bertin faf2e1dfa4 Add a CACHE_VERSION to force cache reloading on version change 2013-11-07 00:26:06 +01:00
Antoine Bertin 93360aa1bb Fix find_show_id in tvsubtitles for ambiguous series 2013-11-06 21:08:09 +01:00
Antoine Bertin 8df7780ef9 Switch to 0.7.2 2013-11-06 00:42:44 +01:00
Antoine Bertin 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
234 changed files with 104715 additions and 2386 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/)
+151 -13
View File
@@ -1,8 +1,123 @@
Changelog
=========
---------
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
* Add a CACHE_VERSION to force cache reloading on version change
* Set CLI default cache expiration time to 30 days
* Add podnapisi provider
* Support script for languages e.g. Latn, Cyrl
* Improve logging levels
* Fix subtitle validation in some rare cases
0.7.1
-----
^^^^^
**release date:** 2013-11-06
* Improve CLI
@@ -12,7 +127,7 @@ Changelog
0.7.0
-----
^^^^^
**release date:** 2013-10-29
**WARNING:** Complete rewrite of subliminal with backward incompatible changes
@@ -29,8 +144,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
@@ -41,8 +171,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
@@ -59,9 +190,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
@@ -75,13 +208,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
@@ -94,15 +228,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
@@ -112,16 +248,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
+16 -2
View File
@@ -2,6 +2,20 @@ Cache
=====
.. module:: subliminal.cache
.. autodata:: region
.. autodata:: SHOW_EXPIRATION_TIME
:annotation:
Refer to `dogpile.cache's documentation <http://dogpilecache.readthedocs.org>`_ to see how to configure a region
.. autodata:: EPISODE_EXPIRATION_TIME
:annotation:
.. autodata:: REFINER_EXPIRATION_TIME
:annotation:
.. data:: region
:annotation:
The :class:`~dogpile.cache.region.CacheRegion`
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 |
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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: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.1
requests>=2.0.1
enzyme>=0.4.0
html5lib>=0.99
dogpile.cache>=0.5.1
babelfish>=0.2.1
charade>=1.0.3
pysrt>=0.5.0
-e .
+3
View File
@@ -1,3 +1,6 @@
[aliases]
test=pytest
[build_sphinx]
source-dir = docs/
build-dir = docs/_build
+95 -32
View File
@@ -1,38 +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.1',
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',
'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider',
'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider'],
'babelfish.converters': ['addic7ed = subliminal.converters.addic7ed:Addic7edConverter',
'tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter']
},
install_requires=open('requirements.txt').readlines(),
test_suite='subliminal.tests.suite')
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.1'
__version__ = '2.0.2'
__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.debug('Skipping provider %r: no language to search for', provider_entry_point.name)
continue
provider_videos = [v for v in videos if Provider.check(v)]
if not provider_videos:
logger.debug('Skipping provider %r: no video to search for', provider_entry_point.name)
continue
# list subtitles with the provider
try:
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.debug('Skipping provider %r: no language to search for', provider_entry_point.name)
continue
if not [v for v in videos if Provider.check(v)]:
logger.debug('Skipping provider %r: no video to search for', provider_entry_point.name)
continue
provider = Provider(**provider_configs.get(provider_entry_point.name, {}))
try:
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 -3
View File
@@ -1,6 +1,16 @@
# -*- coding: utf-8 -*-
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()
#: The subliminal's dogpile.cache region
region = dogpile.cache.make_region()
region = make_region()
+428 -135
View File
@@ -1,166 +1,459 @@
# -*- 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 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 alpha2 code (ISO-639-1)')
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.fromalpha2(l) for l in args.languages}
except babelfish.Error:
parser.error('argument -l/--languages: codes are not ISO-639-1: %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', 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:
os.remove(os.path.join(ctx.parent.params['cache_dir'], cache_file))
click.echo('Subliminal\'s cache cleared.')
else:
click.echo('Nothing done.')
@subliminal.command()
@click.option('-l', '--language', type=LANGUAGE, required=True, multiple=True, help='Language as IETF code, '
'e.g. en, pt-BR (can be used multiple times).')
@click.option('-p', '--provider', type=PROVIDER, multiple=True, help='Provider to use (can be used multiple times).')
@click.option('-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 ''))
+21 -18
View File
@@ -1,29 +1,32 @@
# -*- 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', None), 'Chinese (Simplified)': ('zho', None),
'Chinese (Traditional)': ('zho', None), 'Euskera': ('eus', None),
'Galego': ('glg', None), 'Greek': ('ell', None),
'Malay': ('msa', None), 'Portuguese (Brazilian)': ('por', 'BR'),
'Serbian (Cyrillic)': ('srp', None), 'Serbian (Latin)': ('srp', None),
'Spanish (Latin America)': ('spa', None), 'Spanish (Spain)': ('spa', None)}
self.to_addic7ed = {('cat', None): 'Català', ('zho', None): 'Chinese (Simplified)',
('eus', None): 'Euskera', ('glg', None): 'Galego',
('ell', None): 'Greek', ('msa', None): 'Malay',
('por', 'BR'): 'Portuguese (Brazilian)', ('srp', None): 'Serbian (Cyrillic)'}
self.codes |= set(self.from_addic7ed.keys())
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',),
'Spanish (Spain)': ('spa',)}
self.to_addic7ed = {('cat',): 'Català', ('zho',): 'Chinese (Simplified)', ('eus',): 'Euskera',
('glg',): 'Galego', ('ell',): 'Greek', ('msa',): 'Malay',
('por', 'BR'): 'Portuguese (Brazilian)', ('srp', None, 'Cyrl'): 'Serbian (Cyrillic)'}
self.codes = self.name_converter.codes | set(self.from_addic7ed.keys())
def convert(self, alpha3, country=None):
def convert(self, alpha3, country=None, script=None):
if (alpha3, country, script) in self.to_addic7ed:
return self.to_addic7ed[(alpha3, country, script)]
if (alpha3, country) in self.to_addic7ed:
return self.to_addic7ed[(alpha3, country)]
return super(Addic7edConverter, self).convert(alpha3, country)
if (alpha3,) in self.to_addic7ed:
return self.to_addic7ed[(alpha3,)]
return self.name_converter.convert(alpha3, country, script)
def reverse(self, addic7ed):
if addic7ed in self.from_addic7ed:
return self.from_addic7ed[addic7ed]
return 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)
+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)
+14 -11
View File
@@ -1,22 +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.from_tvsubtitles = {'br': ('por', 'BR'), 'ua': ('ukr', None), 'gr': ('ell', None), 'cn': ('zho', None),
'jp': ('jpn', None), 'cz': ('ces', None)}
self.to_tvsubtitles = {v: k for k, v in self.from_tvsubtitles}
self.codes |= set(self.from_tvsubtitles.keys())
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.items()}
self.codes = self.alpha2_converter.codes | set(self.from_tvsubtitles.keys())
def convert(self, alpha3, country=None):
def convert(self, alpha3, country=None, script=None):
if (alpha3, country) in self.to_tvsubtitles:
return self.to_tvsubtitles[(alpha3, country)]
return super(TVsubtitlesConverter, self).convert(alpha3, country)
if (alpha3,) in self.to_tvsubtitles:
return self.to_tvsubtitles[(alpha3,)]
return self.alpha2_converter.convert(alpha3, country, script)
def reverse(self, tvsubtitles):
if tvsubtitles in self.from_tvsubtitles:
return self.from_tvsubtitles[tvsubtitles]
return 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)
-138
View File
@@ -1,138 +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):
subtitles = []
for language in languages:
subtitles.extend(self.query(language, video.season, video.episode, video.tvdb_id, video.series))
return subtitles
def download_subtitle(self, subtitle):
try:
r = self.session.get(subtitle.download_link, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code == 429:
raise ProviderNotAvailable('Too Many Requests')
elif r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+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
+220 -85
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 []
logger.debug('Found subtitles %r', response['data'])
return [OpenSubtitlesSubtitle(babelfish.Language.fromopensubtitles(r['SubLanguageID']),
bool(int(r['SubHearingImpaired'])), r['IDSubtitleFile'], r['MatchedBy'],
r['MovieKind'], r['MovieHash'], r['MovieName'], r['MovieReleaseName'],
int(r['MovieYear']) if r['MovieYear'] else None, int(r['IDMovieImdb']),
int(r['SeriesSeason']) if r['SeriesSeason'] else None,
int(r['SeriesEpisode']) if r['SeriesEpisode'] else None)
for r in response['data']]
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]
return self.query(languages, hash=video.hashes.get('opensubtitles'), size=video.size, imdb_id=video.imdb_id, query=query)
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, 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
+179
View File
@@ -0,0 +1,179 @@
# -*- coding: utf-8 -*-
import io
import logging
import re
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, hearing_impaired, page_link, pid, releases, title, season=None, episode=None,
year=None):
super(PodnapisiSubtitle, self).__init__(language, hearing_impaired, page_link)
self.pid = pid
self.releases = releases
self.title = title
self.season = season
self.episode = episode
self.year = year
@property
def id(self):
return self.pid
def get_matches(self, video):
matches = set()
# episode
if isinstance(video, Episode):
# series
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')
# 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):
# 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')
# guess
for release in self.releases:
matches |= guess_matches(video, guessit(release, {'type': 'movie'}))
return matches
class PodnapisiProvider(Provider):
"""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 = Session()
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
def terminate(self):
self.session.close()
def query(self, language, keyword, season=None, episode=None, year=None):
# set parameters, see http://www.podnapisi.net/forum/viewtopic.php?f=62&t=26164#p212652
params = {'sXML': 1, 'sL': str(language), 'sK': keyword}
is_episode = False
if season and episode:
is_episode = True
params['sTS'] = season
params['sTE'] = episode
if year:
params['sY'] = year
# loop over paginated results
logger.info('Searching subtitles %r', params)
subtitles = []
pids = set()
while True:
# query the server
xml = etree.fromstring(self.session.get(self.server_url + 'search/old', params=params, timeout=10).content)
# exit if no results
if not int(xml.find('pagination/results').text):
logger.debug('No subtitles found')
break
# loop over subtitles
for subtitle_xml in xml.findall('subtitle'):
# read xml elements
language = Language.fromietf(subtitle_xml.find('language').text)
hearing_impaired = 'n' in (subtitle_xml.find('flags').text or '')
page_link = subtitle_xml.find('url').text
pid = subtitle_xml.find('pid').text
releases = []
if subtitle_xml.find('release').text:
for release in subtitle_xml.find('release').text.split():
release = re.sub(r'\.+$', '', release) # remove trailing dots
release = ''.join(filter(lambda x: ord(x) < 128, release)) # remove non-ascii characters
releases.append(release)
title = subtitle_xml.find('title').text
season = int(subtitle_xml.find('tvSeason').text)
episode = int(subtitle_xml.find('tvEpisode').text)
year = int(subtitle_xml.find('year').text)
if is_episode:
subtitle = PodnapisiSubtitle(language, hearing_impaired, page_link, pid, releases, title,
season=season, episode=episode, year=year)
else:
subtitle = PodnapisiSubtitle(language, hearing_impaired, page_link, pid, releases, title,
year=year)
# ignore duplicates, see http://www.podnapisi.net/forum/viewtopic.php?f=62&t=26164&start=10#p213321
if pid in pids:
continue
logger.debug('Found subtitle %r', subtitle)
subtitles.append(subtitle)
pids.add(pid)
# stop on last page
if int(xml.find('pagination/current').text) >= int(xml.find('pagination/count').text):
break
# increment current page
params['page'] = int(xml.find('pagination/current').text) + 1
logger.debug('Getting page %d', params['page'])
return subtitles
def list_subtitles(self, video, languages):
if isinstance(video, Episode):
return [s for l in languages for s in self.query(l, video.series, season=video.season,
episode=video.episode, year=video.year)]
elif isinstance(video, Movie):
return [s for l in languages for s in self.query(l, video.title, year=video.year)]
def download_subtitle(self, subtitle):
# download as a zip
logger.info('Downloading subtitle %r', subtitle)
r = self.session.get(self.server_url + subtitle.pid + '/download', params={'container': 'zip'}, timeout=10)
r.raise_for_status()
# open the zip
with ZipFile(io.BytesIO(r.content)) as zf:
if len(zf.namelist()) > 1:
raise ProviderError('More than one file to unzip')
subtitle.content = fix_line_ending(zf.read(zf.namelist()[0]))
+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 -99
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,125 +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$')
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
: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
# get the series out of the suggestions
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
show_id = None
for suggestion in soup.select('div.left li div a[href^="/tvshow-"]'):
match = link_re.match(suggestion.text)
if not match:
logger.error('Failed to match %s', suggestion.text)
continue
:param string series: series of the episode
:return: the show id, if any
:rtype: int or None
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
"""
data = {'q': series}
logger.debug('Searching series %r', data)
soup = self.request('/search.php', data=data, method='POST')
links = soup.select('div.left li div a[href^="/tvshow-"]')
if not links:
logger.info('Series %r not found', series)
return None
return int(links[0]['href'][8:-5])
return show_id
@region.cache_on_arguments()
def find_episode_ids(self, show_id, season):
"""Find episode ids from the show id and the season
@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
: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 -103
View File
@@ -1,153 +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 [%r]>' % (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:
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'})]
-389
View File
@@ -1,389 +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 TheSubDBProviderTestCase(ProviderTestCase):
provider_name = 'thesubdb'
def test_query_episode_0(self):
video = EPISODES[0]
languages = {Language('eng'), Language('spa'), Language('por')}
matches = {frozenset(['hash'])}
with self.Provider() as provider:
subtitles = provider.query(video.hashes['thesubdb'])
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1(self):
video = EPISODES[1]
languages = {Language('eng'), Language('por')}
matches = {frozenset(['hash'])}
with self.Provider() as provider:
subtitles = provider.query(video.hashes['thesubdb'])
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = MOVIES[0]
languages = {Language('eng'), Language('por')}
matches = {frozenset(['hash'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = MOVIES[0]
languages = {Language('eng'), Language('por')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class TVsubtitlesProviderTestCase(ProviderTestCase):
provider_name = 'tvsubtitles'
def test_find_show_id(self):
with self.Provider() as provider:
show_id = provider.find_show_id('The Big Bang')
self.assertTrue(show_id == 154)
def test_find_show_id_error(self):
with self.Provider() as provider:
show_id = provider.find_show_id('the big gaming')
self.assertTrue(show_id is None)
def test_find_episode_ids(self):
with self.Provider() as provider:
episode_ids = provider.find_episode_ids(154, 5)
self.assertTrue(set(episode_ids.keys()) == set(range(1, 25)))
def test_query_episode_0(self):
video = EPISODES[0]
languages = {Language('fra'), Language('por'), Language('hun'), Language('ron'), Language('eng')}
matches = {frozenset(['series', 'episode', 'season', 'video_codec']),
frozenset(['series', 'episode', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season, video.episode)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1(self):
video = EPISODES[1]
languages = {Language('fra'), Language('ell'), Language('ron'), Language('eng'), Language('hun'),
Language('por'), Language('por', 'BR')}
matches = {frozenset(['series', 'episode', 'resolution', 'season']),
frozenset(['series', 'episode', 'season', 'video_codec']),
frozenset(['series', 'episode', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season, video.episode)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = EPISODES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['series', 'episode', 'season'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = EPISODES[0]
languages = {Language('hun')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
def suite():
suite = TestSuite()
suite.addTest(TestLoader().loadTestsFromTestCase(Addic7edProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(BierDopjeProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(OpenSubtitlesProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(TheSubDBProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(TVsubtitlesProviderTestCase))
return suite
if __name__ == '__main__':
TextTestRunner().run(suite())
-172
View File
@@ -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, scan_videos
from subliminal.tests.common import MOVIES, EPISODES
TEST_DIR = 'test_data'
class ApiTestCase(TestCase):
def setUp(self):
os.mkdir(TEST_DIR)
def tearDown(self):
shutil.rmtree(TEST_DIR)
def test_list_subtitles_movie_0(self):
videos = [MOVIES[0]]
languages = {Language('eng')}
subtitles = list_subtitles(videos, languages)
self.assertTrue(len(subtitles) == len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_list_subtitles_movie_0_por_br(self):
videos = [MOVIES[0]]
languages = {Language('por', 'BR')}
subtitles = list_subtitles(videos, languages)
self.assertTrue(len(subtitles) == len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_list_subtitles_episodes(self):
videos = [EPISODES[0], EPISODES[1]]
languages = {Language('eng'), Language('fra')}
subtitles = list_subtitles(videos, languages)
self.assertTrue(len(subtitles) == len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_download_subtitles(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = list_subtitles(videos, languages)
download_subtitles(subtitles)
for video in videos:
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.en.srt'))
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.fr.srt'))
def test_download_subtitles_single(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = list_subtitles(videos, languages)
download_subtitles(subtitles, single=True)
for video in videos:
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.srt'))
def test_download_best_subtitles(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = download_best_subtitles(videos, languages)
for video in videos:
self.assertTrue(video in subtitles and len(subtitles[video]) == 2)
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.en.srt'))
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.fr.srt'))
def test_download_best_subtitles_single(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = download_best_subtitles(videos, languages, single=True)
for video in videos:
self.assertTrue(video in subtitles and len(subtitles[video]) == 1)
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.srt'))
def test_download_best_subtitles_min_score(self):
videos = [MOVIES[0]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = download_best_subtitles(videos, languages, min_score=1000)
self.assertTrue(len(subtitles) == 0)
def test_download_best_subtitles_hearing_impaired(self):
videos = [MOVIES[0]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng')}
subtitles = download_best_subtitles(videos, languages, hearing_impaired=True)
self.assertTrue(subtitles[videos[0]][0].hearing_impaired == True)
class VideoTestCase(TestCase):
def setUp(self):
os.mkdir(TEST_DIR)
for video in MOVIES + EPISODES:
open(os.path.join(TEST_DIR, os.path.split(video.name)[1]), 'w').close()
def tearDown(self):
shutil.rmtree(TEST_DIR)
def test_scan_video_movie(self):
video = MOVIES[0]
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.name == os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.title.lower() == video.title.lower())
self.assertTrue(scanned_video.year == video.year)
self.assertTrue(scanned_video.video_codec == video.video_codec)
self.assertTrue(scanned_video.resolution == video.resolution)
self.assertTrue(scanned_video.release_group == video.release_group)
self.assertTrue(scanned_video.subtitle_languages == set())
self.assertTrue(scanned_video.hashes == {})
self.assertTrue(scanned_video.audio_codec is None)
self.assertTrue(scanned_video.imdb_id is None)
self.assertTrue(scanned_video.size == 0)
def test_scan_video_episode(self):
video = EPISODES[0]
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.name == os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.series == video.series)
self.assertTrue(scanned_video.season == video.season)
self.assertTrue(scanned_video.episode == video.episode)
self.assertTrue(scanned_video.video_codec == video.video_codec)
self.assertTrue(scanned_video.resolution == video.resolution)
self.assertTrue(scanned_video.release_group == video.release_group)
self.assertTrue(scanned_video.subtitle_languages == set())
self.assertTrue(scanned_video.hashes == {})
self.assertTrue(scanned_video.title is None)
self.assertTrue(scanned_video.tvdb_id is None)
self.assertTrue(scanned_video.imdb_id is None)
self.assertTrue(scanned_video.audio_codec is None)
self.assertTrue(scanned_video.size == 0)
def test_scan_video_subtitle_language_und(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.subtitle_languages == {Language('und')})
def test_scan_video_subtitles_language_eng(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.en.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.subtitle_languages == {Language('eng')})
def test_scan_video_subtitles_languages(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.en.srt', 'w').close()
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.fr.srt', 'w').close()
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.subtitle_languages == {Language('eng'), Language('fra'), Language('und')})
def suite():
suite = TestSuite()
suite.addTest(TestLoader().loadTestsFromTestCase(ApiTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(VideoTestCase))
return suite
if __name__ == '__main__':
TextTestRunner().run(suite())
+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.info('MKV has no subtitle track')
except enzyme.Error:
logger.error('Parsing video metadata with enzyme failed')
return video
def scan_videos(paths, subtitles=True, embedded_subtitles=True, age=None):
"""Scan `paths` for videos and their subtitle languages
:params paths: absolute paths to scan for videos
:type paths: list of string
:param bool subtitles: scan for subtitles with the same name
:param bool embedded_subtitles: scan for embedded subtitles
:param age: age of the video, if any
:type age: datetime.timedelta or None
:return: the scanned videos
:rtype: list of :class:`Video`
"""
videos = []
# scan files
for filepath in [p for p in paths if os.path.isfile(p)]:
if age and datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(filepath)) > age:
logger.info('Skipping video %r: older than %r', filepath, age)
continue
try:
videos.append(scan_video(filepath, subtitles, embedded_subtitles))
except ValueError as e:
logger.error('Skipping video: %s', e)
continue
# scan directories
for path in [p for p in paths if os.path.isdir(p)]:
logger.info('Scanning directory %r', path)
for dirpath, dirnames, filenames in os.walk(path):
# skip badly encoded directories
if isinstance(dirpath, bytes):
logger.error('Skipping badly encoded directory %r', dirpath.decode('utf-8', errors='replace'))
continue
# skip badly encoded and hidden sub directories
for dirname in list(dirnames):
if isinstance(dirname, bytes):
logger.error('Skipping badly encoded dirname %r in %r', dirname.decode('utf-8', errors='replace'),
dirpath)
dirnames.remove(dirname)
elif dirname.startswith('.'):
logger.debug('Skipping hidden dirname %r in %r', dirname, dirpath)
dirnames.remove(dirname)
# scan for videos
for filename in filenames:
# skip badly encoded files
if isinstance(filename, bytes):
logger.error('Skipping badly encoded filename %r in %r', filename.decode('utf-8', errors='replace'),
dirpath)
continue
# filter videos
if not filename.endswith(VIDEO_EXTENSIONS):
continue
# skip hidden files
if filename.startswith('.'):
logger.debug('Skipping hidden filename %r in %r', filename, dirpath)
continue
filepath = os.path.join(dirpath, filename)
# skip links
if os.path.islink(filepath):
logger.debug('Skipping link %r in %r', filename, dirpath)
continue
if age and datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(filepath)) > age:
logger.info('Skipping video %r: older than %r', filepath, age)
continue
try:
video = scan_video(filepath, subtitles, embedded_subtitles)
except ValueError as e:
logger.error('Skipping video: %s', e)
continue
videos.append(video)
return videos
def hash_opensubtitles(video_path):
"""Compute a hash using OpenSubtitles' algorithm
:param string video_path: path of the video
:return: the hash
:rtype: string
"""
bytesize = struct.calcsize(b'q')
with open(video_path, 'rb') as f:
filesize = os.path.getsize(video_path)
filehash = filesize
if filesize < 65536 * 2:
return None
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(b'q', filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF # to remain as 64bit number
f.seek(max(0, filesize - 65536), 0)
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(b'q', filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF
returnedhash = '%016x' % filehash
return returnedhash
def hash_thesubdb(video_path):
"""Compute a hash using TheSubDB's algorithm
:param string video_path: path of the video
:return: the hash
:rtype: string
"""
readsize = 64 * 1024
if os.path.getsize(video_path) < readsize:
return None
with open(video_path, 'rb') as f:
data = f.read(readsize)
f.seek(-readsize, os.SEEK_END)
data += f.read(readsize)
return hashlib.md5(data).hexdigest().decode('ascii')
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 |
H4sIAAAAAAAAA9VdbXPbRpL+bP2KCa68lCoiIYLvlsiUXmxZWSvRWXJ8Wy4XCgJAEhYIcPEimpvK
f7+newASA0mOneRuKtqNRVLTw+6enunuZ3oGR9+d/Xx686+rl2KeLUJx9e7kzcWpMJqm+b5zappn
N2fif17fXL4R7daBuEmcKA2yII6c0DRf/mQIY55lyxemuVqtWqtOK05m5s1b8zP11Sbi4mUzq1C2
vMwzJjtH/IWfF2GUjh/ppj0ajSQ1t/UdDyQLP3PAaLZs+v/Og/uxcRpHmR9lzZv10jeEK9+Njcz/
nJnU/6Fw506S+tk4z6bNoSFM9JIFWehPdq59J3HnwjhzwtBJDXGd3/JfxFm8isLY8URTHHte4A58
r+XGC7x9FScCrUU2j1Nf3Pwirv0k8NOiWfpi00e6L27uy786kScu43tqeOOEd/vUTb4Q9PEiTnzR
PDILlo7CILoT88SfKipxKlyYbpqaq+AuSAt20xY+MUTih2Mjzdahn859PzMEdzk2PH/q5CG9h4oK
zTABaYK/jimvX//89ub03Y24OP0Z4ypZmDr3AXTawj+kObFzlLpJsMyqfX1y7h35qSHSxH2a70+p
uYjjLI7DtHXfbrXbrU+pMTkyJfFkZ+fInMtRvo29NZhzMbB+Mjm6TeSoObehL27jxPOTsXFAVpEl
kx0hjjJPJPEqXTrR2BihS5gIa9DE62AxE3M/mM2zsWEdHBhiFXjZfGx0enj9RX6DhTPzU7PUPfHm
J61Py5khKlyIUs81S/nv3AmDbL01CDGF7ZDFzMFqMfZkEgYMCrz9cXITOnTwX0YTxCSVkF5IKeBg
BpVIPdLsCKWOLJ5R7clRuoAxTzbmPk18X2zM6iHD0oZbGDKm+0d0my4PySj80HczETkLGJizXIZO
BCW5mFSY2UG0zLNTjLoh4giTMZqhkfx9vFy+wfvdvUNDBB4+jRe3MX2CcYuXtMyIeyfMqdPEmBwn
zm3gHpnyL/UWrmNMTp3MCZ0HLSR7Pr6gfGWU/fqRMXkZzcIgnT8gK77az9EkT+/85GHPRZMpuHuV
+JDuqU5m4O4cw+EGTvRUG89HGz9ZPN1ihu85xxjdPdXFHLy+zqOZk3zhe4LMmFxAUV9oMgW7V36S
fqHJMkST+EuKW+J7ruIky2e5n/pPsXwLmbatxO5J4vwnIN72nqJIYmPyNoaavsBdAkW8zdMvCYCZ
N7nGkvGFoU8xItcr31OamNKIMOHmbZ5wHs29r511XnDPpj4/IZPeefbsGdavPORXz7Ac0+JVTJzb
PMswA1bzIINrK1a0yF85rhvnUdZazpdgDzM8X8oFANTU4dd0E8azIJI9vKGX39xBSqtYwQK9/OYO
sPBwHyZM7ThIhOdk/jf3Ugk+sGw1YdR3rQi+b8IOVuEJajYLPdNLjEN18LCIbXxJ+Tmtp8+OZi+W
YZ7GERbG4D9YiRYwh3xBfmvzF/JB6LP0OgVT2SrI4L4ocNj4EDhhJ5khGDHsWyySd4V/Kl2ShWEu
XFUHr7/GOxXfYifk4VpLWnfZndzIbzeqnko6CshUchokLkI3/Nvyp1Abqe5Fv98fbPmV/vNP8IfO
m3XeLt6ePsbXEc0NDmDGxhThV/ZCMOWhWJIPjmayoxftg+eHmDrk+Nnn0S+KA4oR5HCgdHUWBJgm
8EsPlDl1XP82ju94fDDEmASpGQZ3Phn1DzzbaCDT553j59Yr/J8i3CoRPip99j+cxfIw9SNvPHXC
1Oe3obOO82wsJ7HNM5Y/l7rs9g4kEeaOTb2mFVLHJdc3Jma4EYSJk9Sd+3Cv8OjzjD+dItYdZ84c
KyG/LyzHavM7eOELb9y22h3Lag/7gzbZk5vEISK+2diIEM+xXrbhVKl6+cmLCCZ/KOJ7P8FYrF7M
A8/zo0MZPb0YHiw/Hxa2+sJq4w2ZHdpxgrB0yBeuEWsmOVbQI1MOQTFNqmOGkaOgjmZcEewhCty+
+q7Z/BBMRZiJi5di8JHGmO2jGn5SKIs/wHb2BcV6v4pbf46wNU5eiDwJdytLBFlRGcubiEYDH/Nl
GnxuzTN3T/yG3k3uHux89wGjGUw/Npt4U8SfJV9HYEtcOsmdn50H3mm8WMbIiXxxjbmdCaYoV/lN
q2sOm98i/D1tdzrDHrFMa8aDhlcIxpF5KK2oJf1g3lKUtJjZkER2o6YKi1nAiQrmL8YtC1K4cPOH
PFvYaZwnrj+G8WH1+Qd9ItexMSVK/N6FDTsULqLvh8vUVQgbFbBopD8eBj6BP0fMeZtMJGsPl78N
Lw87ewMBYYWtFiJJhztgZZQ9sahyga5+VL4uUhCpwPLDp36vgsiLV63L8/cs+Q0lfqw5MRbGVRIv
4sz3RJFEGodlN7vTPOJJuLv36wPeykZP/b53EnE2hqMWZ/Bpu3v73tiL3XyBbGb/dtyg5Kaxj8Fo
uImPBi9DzOsoa+w77riBaQvDO50HodfYT7Nxgw2yse+l4wbCEETWoI3GDZqdjf1ZMG5gPIseTtYX
XuPwm9mtikGsB2Pvg+t/3G3IWdvYOww+pNnHD176cRwdeh9mwcdd42nD3vvguB93g73DLFn/yv2t
xgEWWU7T38vhKNVxGKxaMQRGAoBXqwSzKIx2DQADxvfGIoR9xXiBXPDI3L4q/2gwkRsiEQc9fZM7
DlYfbj8eYib/RT+uk7nzXX+vFMQrvuf3tXD4G3Hk3RNLrE3MdKjSu2/RFL48ty/ODLyrahZ/iiI/
IbBlzDZ66LIuvfu9PzeqTymDOEw3DMo0HDymLSddR2SM9KuB94AQkHI3+Be9LyGVBmMq9AmHKiYW
Vbe1mfiO6W0CCfKyLRYKSf8PiH++P2vBcv8FBGZ3T76+hInMyzdy5sg/vMaqk2KIpTbSvcPf9vDu
Lxvjuna2gET9L3/V+yL6/Ku6q/fzlH96CbiJvRP5r6t42cwjABpVtyVNoOpdK+DOZIfs5fL89Axr
53Z1O9wpZ7OcwLsNWqAbO983nsaJ6K8yICOTyb9gMsPeiGAiWAxo6Lu3VrN9X1jO9gNpPdv3hQXh
W4FKFHCgIfFALC3ELnM7gfETCiXVQFGAoqiN/spYpQgMdjZenFY5J8AcNgCV0bA8O+L4pszo8MYP
B8CdtlHX1o2hLbwpf6ckkoATD1l1iNHOq3QI5CVO+CsZ6vqqnCFszYKpAVCtCKCr3eM1vuDpv/zJ
r06e/uoijK8wU0b0m49I9qdZw4gBYwOuKuNnA5k6AbycpQogx/MYSy9WHQRfPGQqPIYPGaoqYCxJ
W0VNJUZVfi6zQQIRCxCoRJEfwF7mRAJliC+rXwC0eQEopphtSF/5XaleF4DZCcDSaNO/RKvlqJGY
JS6K+HTSHnaA/6aAeVOgdpjXcJiwJQ6iNlYqw9hnlfi6NOR6iCv5lf9+tVUhgXdunRT5EyWhAIuP
CGws5QEYnYcAbnaOgXcjjhQhci8EgQwybnNnkzABc3iAbGVCQClUNpGKRRaBkXcKXf6AWYre8UkR
lUspN7I+nHaUoChoaJHUDg+eq/ORjI7NjLKJr5pOQD/vgxQpWyk6Tyvuo8y0EYkHviklMdv431kw
m/nJc2uQ2mdOjpwOi4bw/NscyVmnPez1jUJubDoctD8ftPFb0jRSxJWSQgIcckoQ0+V/mCP/h8xb
5hs/BSIC5LrCMFIaKV7BsIXfRTs9bHbM6+XaDiI7m/s2PADAvCq/3Rq/HfALAhFE2NzxBRPoYbxr
Upic2vHU/sUHWu5Ersp6p8Z6F6wziYinYkOih/meCWDz1neR+Fe1bdVY7oHlsqEORi1Mwbd+HmHS
2vZza9h+bo0qDI8OlPlnyflXEAix2wY4XWy5sNf6/5l6lmkpTFt1ppU5CKZpDm6ZtjQx3TF/Dj37
2P13jtgoq1nz6ECZiGCaJiIIRJVAj7a75sl6iT0sxTCUyQd2afLJZnqYxIQDNnNny4zcPnFu1wq/
7erMA78884iigLAwDW/Xeljvm2dxDvjPfu8zvqvwjf3drT8B333oWTYXRXM9TA/Mt3nkrJyqlocj
7Dor3A5o5sl2etgcmkCFOA6uaHU4Gtb4HILPsqEeRkfmdZ7cBwiiFUYHNUZHYLRsqIfR9oF57Gbk
ld8AFVeYrTmL9gGYRVtyx9RWE79t84a2gWehymzNSbQpsiwbauLUMl9h98CP7AuPygS2UeWo5hza
5NFkW0FtNfHbMf8ZeBFhtYDKK9zWfEObXNmmpSZeu+breOHbxzM4X4VZJSSzPrfJkVFTwU01cdsz
sXPLE8zGbwA7vrrO1rxZm7wZKHiaCfxmCk28980f8zDwOad762d5ouq75tDa5NCYAAmdbK6J74F5
gwzpre/ZrwI440fC4eGw5t/a5N9ABcY9QVQaY+L28CH/tch4OKz5vTb5vRr/usLjNnlA335JCyDb
znWQ1tCAYc0btqU39OG6QQT7kSR67Mc6ME+xetvnQaKs3cOaV7TIK1JLQS018YqsL147IUXL8I0z
xTPSrmw1iLPIM3JzCpW5uSauLbbwn/MsDYDYV/OR4bDmIS3ykGTZm9aaeO6YP8bzyH6JHdiZfXFx
8fi6UvOYFnlMohNMJ0CncWWxunUZgBQ8WFpqftQiP1oTYVfP2tIByPF+ji1mFAPYr3lrGYt8FnOS
aBOPzzuvHh2XnrJediT8UXYlyq5EFnP2yOL+oG2gOoBFSt6+LGZ97HrKugoxafKUXT0tpq7R7PAy
cA13G2X2PwOs/QpmPFSCI0hDk4mWAkmBSJQodCwHHbNrXvsod8jUtUsJicAwTZ2inR42e6zhfybw
wACOj6dTJ1BUPFDiIHBM8SepuCARkkQP831m/gwJp40aYOwsbTOTQX0+U/BJfFNrQa31sCxDzzdx
mtlc86LwXJ+cZcxJzQU318P0EDGE56OArqJfJdaBXVB8ya30sDgyLx0EkW4WL9aPLvADJdYBwxRR
bmk0ruWAWbZ8MO/1VXughDydzwy3bGnAu64Fug3IhTab/ACbscpSN1ACHfDMqAsmYNFWj5m0LaCE
SFnta+feT3mj7EwFNgdKdAO+yUMyjWAa3isDjSb+O+ZlnKEakVA5/LapnkiZlzWXyIiMJCFwDiSC
SDRx38VuDsEENk6gIBehKkDVaGr+kSEaSSIqJJq475mEbdo4xPZoNtWvuUqGaoiCjz7pTKgQFvfN
q3gFs0FZq4Iv9Wt+kkEabiqoqSZND8wrBPFJhONk9nVONSNbz9OveUlGZjbNAZkHmlw7YJkf/Shy
noDD+jWPyYgME2iFw2AbdTjmdB4H6gZ8v+Y8H8AxkkSPtQCOQQgdY1bezGMqE1GWlH7NdzIoI9sj
HpTtNfFNNTH3MarUbbtADa7pMKti7DUvyghNQYUaMYkZMJUmGSwABu4dmzxtqYSPxl79mk9lyIbo
YPlMpTH6sgi2eSBBPQLr19xqAdsoEuiKwQDaUAz2Hmegke3aZ75TS5P7Na/KgA1IkPgziZAkmiyo
Zx7LYiUbdg20T9lg6tV8qkXp57GsVUJ1GLfXwXcXQNNPMbZrEt++ZPTa/glrpn2eb71r+2BooVqy
grB2Jaz0U4xdGxQFSkJBhAKEeuSwnpLDxqGmzLY2qxGJM1QmMsSh4PgJcXCGDYeDLB1SDTE6Evux
ncx+QwcQN2L0R/2eIsVQDopsL5xMcHsdbI/ANs3kV84iCNfSJWz47rRHHSXyGUm+aR5LAukN9DCO
Mi0sojbqd5GAKzwrUQ94JouhtkK21cMu4Yo4RWO/XNNtBJvYctRR4hxwKwFFNBXUVA+zlDSleZI8
KG4ZdZTYBuwSnFhtrIfhnolMmTZBzhOcoVP0q4QzYJiWc9lYcGM9DPeLpFq1BWWVAK+EIMpMWpMh
SOyQ6l4p+z/liyUU7SpBCjgu8UMioexfkujR8dDE/SCR5yRbB0lLmuIfwTLBiGVDPYyOzFM6YQeI
go8wq7uPIxTNVhw6GCYYkQkAUJQEehgHjnidT3FujYGhNziLj9oGRo1xOLlqJ5aS848knChJGSCS
pBJwBqkmaaQvvEpwB4HCfM0JbnBFbqmJV6CKkX2FwllH2YUYWTXvJ8HESBRNNXHbMc/j2Ltdo3Dn
FMFG4q/8MKTXhGwdA5RT9F1ziQwoFvS41qmg3meMi2g1ydQ1T/MkoGPpa7lRiG1fhFK4H0cRpuYw
GV/cEMr9Qo/nAAg1SSJ35JAbpORC3+B2LEWCmgdlkJFiwIJAEIEmzrEdtwKiu7Zf4fwq5XRK4cbI
qjlUhholBSracM6eKTTxLn0rEmEPkfc5rs9QlF5zrJtiMNleUHtNfA+pGB7VjgHqdCPE37M4cxTW
aw6WUUcmEQHKdSOE4USiiXsgj3MH1xWwxpXoq13zsxJx5Masbk0hGLDGYqJVldyuOVTGGIt2ejRr
EbYIbOIatxAGCiA6atccaAEpAo8oGmvi2DJ/pis88kSZeu2aB2XosGyoiVOcs0NwRavzWe7eqWZb
85aMExatBbfWxHOX9pj/neNKN1y6Vsl42zWPyNjgtqkmbnsoLEaFxBn2lGESqoZrDpARQWoNFFO2
1sRzH0BaNodVMBj+kirQcKHcFFm7ou+aE7QoqywIgYYTmSjINMkxMH9svW3Zb3G+l441b8GRds0L
WpReUlsh22rid4jdH2QpXrBE7KfwW3N9FuWWlbaa+B0Be0oIK0a9lYP4bupngRpuHNR8n0U5piSi
w7oOYjxJpEeCzoH5GglDTBlDJZHgz6r6P6h5xQ6VQ3OrfbFJIPi9JjnaFDthmXmFW5E4W0BapkzW
g5qr7FANC9MIouFEgWh08I9KljbK0BM65r2tQrd6XaWou41zhMR02VATp7zJgHs8aVNfPUxGDFdd
PDFcbCoU7bUdKIOGZf0pF/sX9TbvcU/VxsiJ+aqJEPNlCSrX/RcVN0SkSfNdE2XxZbHQq2RNUcuV
s83wSYTqPCURCEUGVVkvJKmAWWjJ7TEKPWwuOJ79Pk62qTDxXV0miW8Ck6mloJaa9N033zsJAymb
WiHCId7PY4BxZ84iQj36DUUJVSMaVT0VSUJBAfphUGVTQYR+sGOLfnAbBvUDl4B+NMk5QOYWr5C6
wYllvHjKcw+ncZJUDrZimEbVsIGEo7ChJKb9NpJLFjJIYk0SDc0z3/X5DmZlbKoBG7FPUcSmpSZe
gVHThbhYTI+zDCfbFYarATIxzAi1bC5kc01cA6AGUoWctG7+1RwEHHN5K1oiIdVn4EVl680Ku/xJ
2pK7sdtMhAy7musR22WFK2gE0Uir1oJUUHQgz1Bd41pcd64YSM3fMijN6uamuoyjg6gykWdJr5wl
qujaCtM1P8swNBEgaaLm3+liG8BzvICZprzon8f0UmG85l0l5CxJeH2XJLrY78mVJAHoWT3NSOZd
c6+MNPOyk6y1HWgku4aLlbdn2CdIQ+wTXJdf1XgPjzzYbtHRvGRvKkkEkSCEB4kujRe7uM4al4Dg
/xW4y+r1DmrOcoM0v3fWuAUE/y/uoaU7hf8fL+IhvctTx0igcX9pJiv+cLtvdWnp4RYzVfXkLHlp
kVRU70c0unTPubfPp4W8pFJpRpqvuU0GnJF04/kQOC1ErTUxDcj5eJkEoU0xU3U3mpiueU4Gnrk1
R1horYvptnlMm4m4NYbM/BxgQXU57x3UfCeD0KAQREGGfh7rWtEty/wXwRsr+wQwzZ1NGUVlQe8p
F2VheWFEWlIIpuAcRJfeixOTdCMiZX3qfRJkMTU/ygA1z1AioZRP24USWGOKUlbO/O0b/7OT2u9x
uf9/FP3XHOqmmpWpBFMheQKVrjHoyT0i7Mfh5PFlnLrxqipAu+afGMHm3SJsx+GAsaTQxXwf1WZ+
yLikwnTNMTFuTS0ZjNTCLB32vkIRFCoOS4DjZyyTb4NKbt3vWIq2ce6PovSCrEQ4QAYMW08qTWe5
Mf/sN36MglmP/er1Mq5selgQQtE+hCCIjGZtQQW/yjSaxqFjniKAvMTRKGifNj6w8kCnW/uBBEpk
AAkIJwOVuMTxKCif9j2w+IBKkwxd859O8ySmn4XirMC7EhqAdwLItq31OKoOYLHrPMLBbS7jwUs8
8k3RuBIbgGuutZQU+zgmRe016RqH0RJU+nPGhMc/fMrxlIrtriTZuxIegHcK4ZmGU6YNjSb+B7i9
K8MhTJnz/UwveacdV5dWSl5IDiXVhhyEfRW0LAnTAufAswKJVpM8XESCUOcGg6KYkBIsgH0K57H/
geptaqqJ2xFuFfa8NR10waFj1eaV2AAME/bFremMC7XWxDOgL9rp4tX9VV6pgSUjURLt4nQ3tcaq
Tm11sUzbSyGqoRDHOKjmymAeEaLhGPeoF5vCYF4p4G0Xx7yZjoqLHNR0ZbCVCDFxPNUliIUd4s+0
W/kGDx/kRecXWC+qnugBmhVRak6WcTFQ7iPEwXMsURwpNnS6REF4j9Nd5F8JmDzFg0mr2AeGo+Zm
GSYjEnKuhEtKEl3s43qh3JvRsW/shWw368mOaj6WYbKyMaawtlnQM9/RMxxh+pdr3HOAM02KzdSc
LINkBYG4XGO7GwS6tN03L+wzbLpyePwjbtWOAtXga16WgbILwSRkLwWJLvYH5vGUtjgucfMkPQNM
0XvNsTJOxs1RCCub62JbwmSnSe4GuOlXYbrmTjd38pWNdbEMjwo7ldZd4NiYolhnrpCU1GSoeViG
yYhamrpcJs+ZGnVMRK1JKMBmVw6q3RAfo+JADRM6NY/LsFnZGvUG2sIElG2iViC0+QKGqul0a1ks
42XUFI+1ASioS8fFFhOOK2Lbwz6Nb5Nt8TEW9W7No26u6ruWFHgMGSh0MY+D36gYDFwkrb6Xf1bU
XVte5HFv2Vpwa11Md5F68OnQqzBWVsRuzfMzLFYcDKW2uhiWxxmu4zyb4xkud+XN5/aPQaSqvBYA
MCpGEMeGtLwFXRCpLnHoyjHsCVA0qb54cIks2X8tOmDQjMnwrHTqpvil6/EKbYBPtB31pEDKNQgk
UC1i4BLQqiQbgf6SyxCmQbh4+gkzC3q+vDkc9qDmMoyX78o9qN32qHeg4xkQZAzWQZvUB5ASiRSd
1aQlsqgiO88B9W2YRvRbuSFjlwj3kLeWD8EpiUu0kok12L8qlIWE1mOZ/kWnfU6qNx2SQFvTVwQi
4LIgFESIDdlMB+yhCiN3TQAwoSwXXKFCbpuc9DtDaxu4KcKUtX5MSOKgFEFHoqIKI68DeYO6VNyK
iGqs7Q5tvzOq3C+viEKQJgPKIMOGJ5FpNzG4jgRugzN23O+T+ji/gCcDbTf7+12rspmiyENgJ1Nz
3l6l1i6WvLXyZeTjpkLaosNND/R6uxx0e3h28WYNqy4HBIPSKDEBrQdIMfm1dqEGqCMJQ6rvxR1A
qE+s3jTd7/YrNb7KMBEeuiWkwkYQahdmSHeiUIk6MMZKlUC/O6rAFIocBIziHhSiAdKopVZAXQRG
ZZx4kuMcByLXrXn12pUKGUUKQkuLkLEk0z4WQE7f4jpnmBaeXFWVwhpsg/SqFFw9WCHRLAGV6Z84
fBYfR3Mri9egfVC5MXArASo2+ZgB09BxXi2lSlVroqdw4SFreHR0ntqnVMWPQ3rboYAgeMLnw/UK
gpC3Lymx21dQah8ROsAX8ePmsHWDE/D4d3uJ5qDdqVwGrowL+XuiLCMxSaldHMoJQ8Acp3E0pRy1
Ok8G7X4lC1GkIZfPhPQ08JJQuzDk9OkeObqL4GWSxNWBGXW3wI0iivT2IGNvz2TaBekXJ7OVw+QD
y6rcpajIQL5dHszWdJxcnfKyTBH7TwHNehoNOVcqSNrA6lc2ahVhyK9ToFLQ87CAHvamBVtTRcM9
nTi1yHtxfIlysgxQ+b9xkANr9NTCTG6eaLEzV6HUbmojk89PpPY73JgKbGJbajfoWBVgSBkicvaS
TDCZjjIGdVzg6s/zIMxsFALaF1EU40m31XHp9Cq3uldlYZfPpAKkYkOqfWBwiOCNn1FlzMX2yNWg
M+w+FroUJwhAQEUxFzpOW9XGw8LVjHz4DXVsx8mi4vK7B5UH6ylDQS7/WD71BrVsRKV/FDry4ubn
3YPibr3tXIcgj/sU3jnls1f/KGJi/XJ0i/lxsraP8TxiN+CguCJM54nJzrupcoKcrEWFVr9MuFTb
nzludR970O10H0NcMEHI0ZftNfNO5Ydwcdg8UB58hrlduV9yOzVQjFKeD5IU2tnH5k1Ctyxcbl3f
sDuq7NkozHPFIbXH4qSd9Y5Jl5PzntMcNdqVG6uGvU7lPKsiAUXvBRndbUZk2gXpmjjLT6fcOLr6
xa8UTQ57g8p1lIokFLkXdHx6kui0i9Izz1o3iI02y9Gwf/AoXIKZQNNYttbOdh8jcFcmgbJICG+v
462/Hva7lZp/ZSAoZidyeAhEtwlVCuENiLWLNZBiOXjylYdt/Cja4ovDfv9RdB7jQmE7i+DgMVce
NvBBp12UIZ51hWNP2G/Dv7I+ehHfVRat/qi7rUpQxofidSamZITPCl4TqXaRqOAiQrpK9nZMlZ/8
wqXjx8rTwkdtq1I4rYjGlY3cyT6CXnSBX5sOtAuIUB5FIR6co3sXR1jfNqvCqN19NLsq6h6JCjvs
BZV+OdomNkO2G+2jNjZBH8G6isJHaqufZ6B1AR6EvWCEi0LYivKHlUukq/bExY4FGcFbRKZfko55
En/GjQmV7GmEPd3HInaMADl4bq8/eUJw2MWdoN4Md/NttW+1n1ioOD4vmuvXes88SfB4JPsnf0WX
blQuPBlZ7crDBBT7IZfOZAJkdAPHX3LlyVdVAFgD3Gm3qQDgdyUafZKvUVaPC3rzWzwk66Ddeawa
YDnZOaL/zMxBxR6/8CY7eLyG8nMkT9zW/sJncJW2R1kyEUffNZuCuxNTHHjF4ZZmU+3yGfoTLu4f
SscGN3TjJAIMPTkKFjORJu7YmGfZ8oVprlarFgreA3fgey03XpgBbZKl5m3YmgUoxjbpIHCNLeL8
cYaLv/zJr06e/mpVIxul8veaXnAP/ZJyjr30zveXfoLj80u6t5Wue6AnBpCijtBOBN7Y2LS6dpNg
maGKPDu1gGlhDWalP2yIsyEhTmwqrei76efI4U4XMxv6lN0IFL9Mx4aBwcJTArKxYd+GDq5cnSBT
wGMZ1lSiAI9E0QNPzFsMbvGD3iRxMU5OKREGqZXfmQ/7fAPG4AtbrRb1Rd0olkMfHEkV0cv6z1HK
OlDNqN6ofI+H0OJyltbl+fvAg1w3Aco5WWIxFsYVLq+CUXoE/Geo1TQOS7LdaR7BdcTR7t6vD3gr
Gz31+x5VoGfjCNP/DBvBu3v73tiL3ZzKvfdvx43b2Fs39l1/3MBzMdHgZcgPAGvsO+64wc9q9fi2
6sZ+mo0bKT0Kp7HvpeMGajiXSPca+9G4EeEB6Y39WTBuQKqih5P1hdc4/GZ2q2IQ68HY++D6H3cb
wTTBst3YOww+pNnHD176cRwdeh9mwcfdLxjk3gfH/bgb7B1myfpX7m81DmAKrGG6gR3DUarjMFi1
Yhyj38V3rFqrBNYfRrvGEYqcvzcW4eToNsYLb42ZvX1V/tFgIpcuKQc9fZM7DlYfbj8e/vandFDV
h+vg0pBdf68UxCu+5/e1cPgbceTdE0usTUxlqNK7b9F8vjy3L84MvKtqFn+KsPS9vrl8M2YbPXRZ
l9793p8b1apE1dfEYbphUE4r8Ji2nHQdkTHSrwbeez6uU4cB0i96j4MACY7NjRt5Nm0O6RNeqE3z
U+pijS5WtGL+O6ZnVtftFsvW+pT+gGXm+7MWDJgexbe7J1/j3HU2L9/ICST/wNciY6SlUtK9w9/2
8O4vG+qqYuj1kfkt60yd+mvef2GN+xry32vzlHtBfMXOZWfnCBsL8MmTI+mhb+MEDxkfGweGWAVe
Nh8bo4PncDDkyfHPxk0jsEl5IS2cdNEYJ4zhH+iEwtig11/juzHf/XvcsBlHsr4Rx9XHxs0v7My/
1v8HEfWAiEX6Yu5CUA/HRbAgw4K/oQBX8TIPMUfpxgtsH1Bw8zeU4l3qT3Pgcn9T9l/FSU6bN8y+
KaeCnA+TTdQlY1s4+BxTogyHzJSGrbWcL43JSYKXvsCmQzGWDlwaBYEbtXxNZ2Z3RAgcclQsknf+
uthS+uZ+0mbqzlHrjyMtk5tfJEviuvhI5l3f0mclPk/z22bmhHetyM/MLF4Grtk+6LSbcBjAZsJ4
ZhoTXAPtF49gwQcPvu6bVMy1z018a0aBXVpV9CWVRf9RDeGWH6o9fB0vUHEVFVfe/HU6sQbDbnOa
cCFnFq6bDjym10RZJ54+jqoVKOnV5o/YpsIf8TCY4o8PFAbDfNQQr/PlMkYi8Q5KqLD+TfrFEDbz
JSUSdKJ7chMvxbvy7Vczos4Is09XUVziQdpJpOwoVnh8SqS319fiFcoKVYmeao0UaBHkC8IDcakL
lVdXvuKb1AA7pXn8wwJcI5ZfRawRTyrkrHj/h22tS49EPQ/9AmiqMPk387nm5CbPYipbgi54vXxq
aDYmUawd6rqBdNLxmv1mu9luzbNFaEz4wDGejpEsnLJMvqKmPz6WSNJg1sc4P0ZBzR+16C493hDn
5u7ksT/AtpTYYSZ/e4dfXks7nWFzluPQdjOLmxScI4tu4rbC+WYJ9L0gM5fARWx7advA2Xq2Peen
kZ58/9w6IRrbniLvTRFyu3M0+S/EgMmaWtJ6t6Kbd67Rap4Ar/4PwIhybf1qWb60dNUGTWxiy8eX
sLqZEARUNRU4EzgcnOu+I9tgN/3HHGvbGmCnHQ/hwHqP0kGx++66QMgqHD9lzO/nuL31mB4csdXV
V5DVhasKZiJP9j/zmjNDvgjeNn6apH0wFt82AwA6YL8bEBpBEkWE8oY/BBIiP3zwDU8Jv5GCwx3c
tom1DJtM4iSYYe8smtGOU1xe3vAVWsE8xIEFJ0plpbMoVxRl8f5dZh5bVlSV0n6SDBHEn9ZoZdLe
Y+bnt87M+dRKYowKrilOCAGj8ASZDb/3ReDTZjcQcZnv1D6d1Jr94bGgSymKOvbNc1W/Ygikh8el
FTQWpU0/NRAlgErr598hKQS4Sh4EFkRANwfyX4rmb3xUbtFGEO57vMexqG8zw2JODKi26njh41yV
E+EqECrnBcr7LdNiM80es2wEuquMnSavGBlWDIsOOBXL+RybGKg8Fcf3DtDdR6wJZhjMorERwiyN
SWvnGX6+XkUb1kwC+Rw3kykPYap488jX/c5aXyiN3REu9f/2Dp6cjup8q89KrFtyIj72jWQsW5Mp
dkiOzAJAwV/LVzsFPC2y9RIzPvM/Z+Yn596RYBLQFALdZs6P6WscrwcIvbvL+xvpC0OMx6IERlth
DOiRYZEkzmI3DvfED6JoaZppGqK+5IX8oNgaARy6syFnKHU3x5kJF7e77BrPO6eSAUZlGob4fsvD
98KYxfEs9JtO5ITrLHAB8GF7ZeYAo2tIORo1ORrPOy/RZwGR4Y2xh6/fQma/pwREHOJXVgXFKljv
UYWUQBv2zMlaNgH18qNd491xE+cqBr3+8KDZJhkrBC0bF0a5d1fogqYAoYG/iQKyTZI98etvW46A
dhUaeHxgFLjKWQZpSyqFVfEpNZdhngJzh0qQ4v+fQ4NQJW0T4Jso/p3s/C8QW5carLsAAA==
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
@@ -0,0 +1,906 @@
interactions:
- request:
body: data%5BUser%5D%5Bpassword%5D=subliminal&data%5BUser%5D%5Busername%5D=python-subliminal&_method=POST
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
Content-Length: ['99']
Content-Type: [application/x-www-form-urlencoded]
User-Agent: [Subliminal/2.0]
method: POST
uri: http://legendas.tv/login
response:
body:
string: !!binary |
H4sIAAAAAAAAAwMAAAAAAAAAAAA=
headers:
accept-ranges: [bytes]
access-control-allow-origin: ['*']
age: ['0']
connection: [keep-alive]
content-encoding: [gzip]
content-length: ['20']
content-type: [text/html; charset=UTF-8]
date: ['Thu, 31 Mar 2016 13:46:29 GMT']
location: ['http://legendas.tv/']
set-cookie: [PHPSESSID=p599stscp87ik494k66cjt83b2; path=/; HttpOnly, 'PHPSESSID=deleted;
expires=Wed, 01-Apr-2015 13:47:26 GMT; path=/', PHPSESSID=u6fmgu5ivtman5ap13jk1nva03;
path=/; HttpOnly, 'PHPSESSID=deleted; expires=Wed, 01-Apr-2015 13:47:26
GMT; path=/', PHPSESSID=n4mkb9v7a0f57kf2pakc9b9f36; path=/; HttpOnly, 'au=3673290---198d5f2e1a202d0c2878ae871f911763d7cd76c0;
expires=Thu, 14-Apr-2016 13:47:27 GMT; path=/']
vary: ['User-Agent,Accept-Encoding']
via: [1.1 varnish]
x-backend: [default_director]
x-cache: [MISS]
x-cacheable: ['YES:Forced']
x-varnish: ['2027372171']
status: {code: 302, message: Found}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
Cookie: [au=3673290---198d5f2e1a202d0c2878ae871f911763d7cd76c0; PHPSESSID=n4mkb9v7a0f57kf2pakc9b9f36]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://legendas.tv/legenda/sugestao/man%20of%20steel
response:
body:
string: !!binary |
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
headers:
accept-ranges: [bytes]
access-control-allow-origin: ['*']
age: ['0']
connection: [keep-alive]
content-encoding: [gzip]
content-length: ['2895']
content-type: [text/html; charset=utf-8]
date: ['Thu, 31 Mar 2016 13:46:29 GMT']
vary: ['User-Agent,Accept-Encoding']
via: [1.1 varnish]
x-backend: [default_director]
x-cache: [MISS]
x-cacheable: ['YES:Forced']
x-varnish: ['2027372208']
status: {code: 200, message: OK}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
Cookie: [au=3673290---198d5f2e1a202d0c2878ae871f911763d7cd76c0; PHPSESSID=n4mkb9v7a0f57kf2pakc9b9f36]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://legendas.tv/util/carrega_legendas_busca_filme/29087/2/-/1
response:
body:
string: !!binary |
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
UB+l/RdMIdVEpSYAAA==
headers:
accept-ranges: [bytes]
access-control-allow-origin: ['*']
age: ['0']
connection: [keep-alive]
content-encoding: [gzip]
content-length: ['3262']
content-type: [text/html; charset=UTF-8]
date: ['Thu, 31 Mar 2016 13:46:29 GMT']
vary: ['User-Agent,Accept-Encoding']
via: [1.1 varnish]
x-backend: [default_director]
x-cache: [MISS]
x-cacheable: ['YES:Forced']
x-varnish: ['2027372232']
status: {code: 200, message: OK}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
Cookie: [au=3673290---198d5f2e1a202d0c2878ae871f911763d7cd76c0; PHPSESSID=n4mkb9v7a0f57kf2pakc9b9f36]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://legendas.tv/downloadarquivo/525d8c2444851
response:
body:
string: !!binary |
H4sIAAAAAAAAAwMAAAAAAAAAAAA=
headers:
accept-ranges: [bytes]
access-control-allow-origin: ['*']
age: ['0']
connection: [keep-alive]
content-encoding: [gzip]
content-length: ['20']
content-type: [text/html; charset=UTF-8]
date: ['Thu, 31 Mar 2016 13:46:30 GMT']
location: ['http://f.legendas.tv/fa/b1/legendas_tv_20131015154036.rar']
vary: ['User-Agent,Accept-Encoding']
via: [1.1 varnish]
x-backend: [default_director]
x-cache: [MISS]
x-cacheable: ['YES:Forced']
x-varnish: ['2027372257']
status: {code: 302, message: Found}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
Cookie: [au=3673290---198d5f2e1a202d0c2878ae871f911763d7cd76c0; PHPSESSID=n4mkb9v7a0f57kf2pakc9b9f36]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://f.legendas.tv/fa/b1/legendas_tv_20131015154036.rar
response:
body:
string: !!binary |
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
headers:
accept-ranges: [bytes]
age: ['2']
connection: [keep-alive]
content-disposition: [attachment]
content-length: ['34529']
content-type: [application/octet-stream]
date: ['Thu, 31 Mar 2016 13:46:30 GMT']
etag: ['"930e3110a1a4f0a3-86e1-52b301b2041e9"']
last-modified: ['Sun, 07 Feb 2016 15:54:57 GMT']
via: [1.1 varnish]
x-backend: [imagens]
x-cache: [HIT]
x-cacheable: ['YES']
x-varnish: [2027372281 2027372130]
status: {code: 200, message: OK}
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
Cookie: [au=3673290---198d5f2e1a202d0c2878ae871f911763d7cd76c0; PHPSESSID=n4mkb9v7a0f57kf2pakc9b9f36]
User-Agent: [Subliminal/2.0]
method: GET
uri: http://legendas.tv/users/logout
response:
body:
string: !!binary |
H4sIAAAAAAAAAwMAAAAAAAAAAAA=
headers:
accept-ranges: [bytes]
access-control-allow-origin: ['*']
age: ['0']
connection: [keep-alive]
content-encoding: [gzip]
content-length: ['20']
content-type: [text/html; charset=UTF-8]
date: ['Thu, 31 Mar 2016 13:46:30 GMT']
location: ['http://legendas.tv/']
set-cookie: ['au=deleted; expires=Wed, 01-Apr-2015 13:47:27 GMT; path=/', 'PHPSESSID=deleted;
expires=Wed, 01-Apr-2015 13:47:27 GMT; path=/', PHPSESSID=5oe2ekkaploru0oarl8692obl5;
path=/; HttpOnly]
vary: ['User-Agent,Accept-Encoding']
via: [1.1 varnish]
x-backend: [default_director]
x-cache: [MISS]
x-cacheable: ['YES:Forced']
x-varnish: ['2027372292']
status: {code: 302, message: Found}
version: 1

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