Compare commits

...

942 Commits

Author SHA1 Message Date
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 c1fda7f44c Release 0.7.1 2013-11-06 00:32:33 +01:00
Antoine Bertin ce42201eee Improve exceptions in addic7ed terminate 2013-11-06 00:31:49 +01:00
Antoine Bertin f65131e5b0 Use the full path to guess in scan_video 2013-11-06 00:20:26 +01:00
Antoine Bertin 8d9efa5dc0 Catch exceptions during provider terminate in api 2013-11-06 00:19:45 +01:00
Antoine Bertin 1a54bfb732 Fix ProviderNotAvailable in list_subtitles 2013-11-06 00:03:53 +01:00
Antoine Bertin a15c1f05b2 Skip links and hidden files and folders in scan_video 2013-11-05 23:25:57 +01:00
Antoine Bertin ccfd341fe9 Fix enzyme track detection 2013-11-05 00:23:24 +01:00
Antoine Bertin aab8e0aa4d Fix spacing around operator 2013-11-04 23:14:25 +01:00
Antoine Bertin 9b669c8a3d Update opensubtitles unittests 2013-11-04 00:56:21 +01:00
Antoine Bertin 50960fed24 Fix single subtitles being always downloaded 2013-11-04 00:43:16 +01:00
Antoine Bertin ff61bc8d2d Add colors to debug log 2013-11-04 00:34:02 +01:00
Antoine Bertin e5d9c229ed Improve CLI 2013-11-04 00:08:20 +01:00
Antoine Bertin b0e38c7e2c Remove unnecessary personal information from addic7ed logging 2013-11-04 00:08:10 +01:00
Antoine Bertin c12dade5ea Add tests for scan_video 2013-11-03 11:53:29 -05:00
Antoine Bertin cb35dabf31 Remove lxml dependency 2013-11-03 11:52:39 -05:00
Antoine Bertin a66cf4b501 Require babelfish 0.2.1 2013-11-03 11:29:42 -05:00
Antoine Bertin c355d6a24a Fix hashes computation for small files 2013-11-03 11:26:34 -05:00
Antoine Bertin 60c1e93037 Scan for und languages and fix download for single 2013-11-01 08:38:24 +01:00
Antoine Bertin 708126aca3 Fix subtitle language filtering of videos 2013-10-31 23:24:49 +01:00
Antoine Bertin bf763a3ad7 Always log found embedded subtitles 2013-10-31 23:23:36 +01:00
Antoine Bertin bb0c3b91a2 Fix the download loop not breaking when done 2013-10-31 22:49:46 +01:00
Antoine Bertin a7c0cd0d19 Fix intersection of sets 2013-10-31 22:49:01 +01:00
Antoine Bertin f315ef9bd0 Fix subtitle language detection in api 2013-10-31 22:32:43 +01:00
Antoine Bertin b262a5491c Fix embedded subtitles always being scanned 2013-10-31 20:47:28 +01:00
Antoine Bertin d7b30336b6 Fix badly encoded subtitles 2013-10-31 20:46:19 +01:00
Antoine Bertin c834bac460 Use absolute paths in cli 2013-10-31 08:37:37 +01:00
Antoine Bertin 36da0a1204 Validate cache-file in cli 2013-10-31 08:37:28 +01:00
Antoine Bertin 6ece9271eb Require absolute paths in scan_video and scan_videos 2013-10-31 08:36:13 +01:00
Antoine Bertin a465058bb3 Catch enzyme and babelfish exceptions in scan_video 2013-10-30 22:47:13 +01:00
Antoine Bertin bad7dbb55c Explicitly use utf-8 for subtitle files encoding 2013-10-30 21:07:18 +01:00
Antoine Bertin 501aaf076e Handle download limit in Addic7ed 2013-10-30 21:05:48 +01:00
Antoine Bertin 49d27cc7e4 Add login support for Addic7ed 2013-10-30 08:40:03 +01:00
Antoine Bertin e3903f77e9 Use babelfish 0.2.0 2013-10-30 08:39:06 +01:00
Antoine Bertin b6ee9b5d7e Fix required language argument in cli 2013-10-30 08:30:11 +01:00
Antoine Bertin e3301cefd7 Fix language extensions loading
The alpha2 converter might not yet be loaded at this point.
When subliminal's converters are loaded, they import subliminal
which import the video module.
2013-10-30 08:29:43 +01:00
Antoine Bertin 74ac38329e Fix None not being a valid language for embedded subtitles 2013-10-30 08:24:15 +01:00
Antoine Bertin 50c39382e6 Improve error logging 2013-10-29 18:07:26 +01:00
Antoine Bertin 9a65708855 Add missing age implementation 2013-10-29 18:06:57 +01:00
Antoine Bertin 4ebcb2cc95 Fix video_types in some providers 2013-10-29 17:26:56 +01:00
Antoine Bertin 0c3c41fb4a Fix unsupported type for timedelta in cli 2013-10-29 17:26:29 +01:00
Antoine Bertin f44942f78e Switch to 0.7.1 2013-10-29 17:23:41 +01:00
Antoine Bertin d7f2211800 Update dev requirements for sphinx documentation upload in pypi 2013-10-29 12:59:50 +01:00
Antoine Bertin 60610e2032 Merge branch 'develop'
Conflicts:
	requirements.txt
	setup.py
	subliminal/infos.py
2013-10-29 12:43:20 +01:00
Antoine Bertin f11402c452 Complete rewrite of subliminal 2013-10-29 12:22:21 +01:00
Antoine Bertin 277b046b41 Fix requirements for enzyme 0.3 2013-05-19 15:44:49 +02:00
Antoine Bertin 7878fb2f92 Update unittests 2013-01-20 00:01:52 +01:00
Antoine Bertin f90c4634c2 Revamp TVsubtitles
- Use dogpile.cache
- Compute confidence based on votes
- Improved keywords detection
- Add docstrings
- Rename to TVsubtitles
- Add unittests
2013-01-19 23:26:40 +01:00
Antoine Bertin b52fbc8af5 Unset language_code in PodnapisiWeb 2013-01-19 21:42:34 +01:00
Antoine Bertin cde1175a7b Update default User-Agent 2013-01-19 21:42:06 +01:00
Antoine Bertin 30786743e3 Update TheSubDB and add unittests 2013-01-19 15:25:45 +01:00
Antoine Bertin c675bad95c Update Subtitulos and add unittests 2013-01-19 15:03:35 +01:00
Antoine Bertin 288b92d2ce Update SubsWiki and add unittests 2013-01-19 15:03:06 +01:00
Antoine Bertin f6e656117c Add unittests for PodnapisiWeb 2013-01-19 15:02:11 +01:00
Antoine Bertin 896baa6a04 Add unittests for Podnapisi 2013-01-18 20:54:37 +01:00
Antoine Bertin b8f820a9af Fix download method in Podnapisi 2013-01-18 20:52:52 +01:00
Antoine Bertin eb8b046dbd Use new is_valid_subtitle method
Thanks to byroot
2013-01-18 20:52:13 +01:00
Antoine Bertin ff9e0f3190 Fix Video path being empty even if forced in unittests 2013-01-18 20:51:27 +01:00
Antoine Bertin 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
Antoine Bertin e4f10b9e77 Update addic7ed to use dogpile.cache 2013-01-13 21:25:13 +01:00
Antoine Bertin 5ca2623c13 Merge remote-tracking branch 'rik/requests-changes-172' into develop 2013-01-13 16:47:59 +01:00
Antoine Bertin e560823271 Update setup 2013-01-13 16:46:27 +01:00
Antoine Bertin 3f83b13a29 Use setuptools entry points 2013-01-13 16:46:20 +01:00
Antoine Bertin 507bf09466 Update version to 0.7-dev 2013-01-13 15:57:34 +01:00
Antoine Bertin 688a07dbe9 Update requirements 2013-01-13 15:55:36 +01:00
Antoine Bertin e7c4bc9fc3 Move tests under subliminal module 2013-01-13 15:53:24 +01:00
Anthony Ricaud 04c74afad6 Update to the latest requests API.
fix #172
2013-01-13 14:30:33 +01:00
Antoine Bertin e8a4521b7e Add query unittests for BierDopje and OpenSubtitles 2012-12-16 21:31:49 +01:00
Antoine Bertin 768b9d203c Update new unittests
- Do not expect the exact count of results in list tests
- Download tests
2012-12-16 10:33:32 +01:00
Antoine Bertin ba5e03342f Use key_mangler to fix the error in dbm backend in dogpile.cache 2012-12-15 13:33:25 +01:00
Antoine Bertin cb3ff0334d Add unittests for BierDopje 2012-12-15 13:32:38 +01:00
Antoine Bertin b96bf9b9a5 Finish list unittests for OpenSubtitles 2012-12-15 13:08:05 +01:00
Antoine Bertin 4bf673dfc6 Update new unittests 2012-12-11 14:55:01 +01:00
Antoine Bertin 223fe8025c Use dogpile.cache 2012-12-10 23:31:44 +01:00
Antoine Bertin cf47cf1512 Update requirements
- Add charade and pysrt as test_require
- Add dogpile.cache as install_require
2012-12-10 23:27:57 +01:00
Antoine Bertin 1bac0c3e8c New unittests for services using a yaml configuration file 2012-12-10 23:10:15 +01:00
Antoine Bertin 4634c353d8 Fix subswiki on some movies 2012-12-10 14:05:33 +01:00
Antoine Bertin 9ac2dde4e1 Shorten travis install part 2012-12-09 16:22:58 +01:00
Antoine Bertin 07a6db43e6 Fix test_videos by using unicode 2012-12-09 13:34:17 +01:00
Antoine Bertin c40f1f0cc2 Update unittests for PodnapisiWeb 2012-12-09 11:12:06 +01:00
Antoine Bertin c4ddc6d793 Fix subtitles validation
Subtitles are considered valid if their first 50 lines are
2012-12-09 11:11:48 +01:00
Antoine Bertin 8f58974a1a Remove keywords parameter in PodnapisiWeb as it is not supported 2012-12-09 10:48:07 +01:00
Antoine Bertin b17fbd6fc0 Ensure paths are unicode in get_defaults 2012-12-09 10:46:47 +01:00
Antoine Bertin 57d5d57c53 Fix subtitles validation 2012-12-09 01:14:13 +01:00
Antoine Bertin 5436512b1f Allow empty keywords in services
Applies to addic7ed, subswiki and subtitulos
2012-12-09 00:42:32 +01:00
Antoine Bertin 40e10ea72c Replace filesizes checks in unittests with subtitles validation
Use pysrt for subtitles validation
2012-12-09 00:38:34 +01:00
Antoine Bertin ea6aba4e92 Skip badly encoded paths and require unicode for api calls 2012-12-08 20:21:45 +01:00
Antoine Bertin e4817dd0d3 Update filesizes in unittests 2012-12-07 14:55:55 +01:00
Antoine Bertin 751a2d822a Fix PodnapisiWeb service
- Remove the sR parameter that does not accept multiple
  values and is case-sensitive
- Ensure the languageId is an integer
- Update filesizes in unittests
2012-12-07 14:55:43 +01:00
Antoine Bertin 38cc8422b6 Fix PodnapisiWeb and add unittests 2012-09-23 15:03:06 +02:00
Antoine Bertin 35f2e47195 Fix unittests for SubsWiki 2012-09-23 14:53:20 +02:00
Antoine Bertin ab3ad0fe70 Merge pull request #113 from abenea/podnapisi
Podnapisi
2012-09-22 06:37:13 -07:00
Antoine Bertin ccf26730a2 Fix repr of ServiceConfig when no cache_dir is given 2012-09-22 13:17:36 +02:00
Antoine Bertin c23868dfb1 Merge remote-tracking branch 'wackou/develop' into develop
- Move get_defaults to core
- Add new functions to __all__
- Fix download_subtitles in api
2012-09-22 12:43:03 +02:00
Antoine Bertin b24af17326 Add python 2.6 compatibility on download_zip_file 2012-09-22 11:53:37 +02:00
Antoine Bertin 45aff11bff Fix Subtitulos on incomplete subtitles and use bs4 syntax 2012-09-22 11:45:36 +02:00
Antoine Bertin 1ee700fa9d Merge branch 'develop' 2012-09-15 13:29:27 +02:00
Antoine Bertin 4f74dc9031 Bump version number 2012-09-15 13:28:11 +02:00
Antoine Bertin b53fd0bd61 Fix enzyme import in videos 2012-09-15 13:05:58 +02:00
Antoine Bertin 80e3514d56 Update copyright notice on Addic7ed 2012-09-15 13:05:35 +02:00
Antoine Bertin 3f1cac3ccc Add Galician and Catalan languages to Addic7ed 2012-09-15 13:05:21 +02:00
Antoine Bertin 261e4e8f67 Fix OpenSubtitles testcase 2012-09-15 11:58:20 +02:00
Antoine Bertin bec7ec1901 Fix SubsWiki 2012-09-15 11:29:45 +02:00
Antoine Bertin 69015293a4 Fix OpenSubtitles testcase 2012-09-15 11:28:58 +02:00
Antoine Bertin ca55e417ee Remove unused function in Addic7ed 2012-09-15 11:28:40 +02:00
Antoine Bertin ed37415ee2 Use relative imports in Subtitulos 2012-09-12 23:59:31 +02:00
Antoine Bertin 68dc99f7ab Fix Addic7ed 2012-09-12 23:51:17 +02:00
Antoine Bertin 4d9cac8941 Fix unittests for BierDopje 2012-09-12 21:52:59 +02:00
Antoine Bertin fbd6fe00d6 Add a user agent to BierDopje as requested by the service 2012-09-12 21:52:40 +02:00
Antoine Bertin e491680dff List supported services in CLI help message 2012-09-12 21:29:11 +02:00
Antoine Bertin ffc8474918 Test current directory if no folder is given while scanning 2012-09-12 21:28:43 +02:00
Antoine Bertin dd7f26e51e Update diaoul-sphinx-themes 2012-09-12 07:42:13 +02:00
Nicolas Wack 498460031e Fixed missing import 2012-07-30 00:25:57 +02:00
Nicolas Wack d977e3569f Fixed get_defaults() function call 2012-07-30 00:25:05 +02:00
Antoine Bertin f122e7e4ed Merge pull request #114 from abenea/ass
Add the .ass subtitle extension
2012-07-15 02:13:40 -07:00
Andrei Benea 3af3db3df6 Request XML search results. 2012-07-09 21:35:46 +03:00
Andrei Benea f4246de8a7 Add the .ass subtitle extension. 2012-07-07 11:20:54 +03:00
Andrei Benea 43b647ed28 Enable the PodnapisiWeb service. 2012-07-07 11:09:18 +03:00
Andrei Benea 04958f63de PodnapisiWeb service based on the simple web interface. 2012-07-07 05:03:43 +03:00
Nicolas Wack b15b2c0e7b Factored out consume_task_list() to avoid code duplication 2012-07-01 16:39:45 +02:00
Nicolas Wack 61909b68cc Factored out get_defaults() function to avoid code duplication 2012-07-01 16:37:03 +02:00
Antoine Bertin 71c91bed29 Control subtitles naming in unittest 2012-06-26 19:49:34 +02:00
Antoine Bertin a8aa57dcd6 Merge branch 'develop' 2012-06-24 22:56:03 +02:00
Antoine Bertin a7de8c81b4 Bump version 2012-06-24 22:55:47 +02:00
Antoine Bertin c2688fe81c Remove references to Podnapisi as it is not ready yet 2012-06-24 22:54:42 +02:00
Antoine Bertin 295474506b Update NEWS 2012-06-24 22:49:24 +02:00
Antoine Bertin c4a989dd3d Add the release name in the repr of ResultSubtitle if available 2012-06-24 21:48:18 +02:00
Antoine Bertin 7f6e192149 Add more logging in matching_confidence 2012-06-24 21:46:54 +02:00
Antoine Bertin 28b40e9174 Fix subtitle release name in BierDopje
Matching confidence could not be computed because of the
missing extension
2012-06-24 21:46:33 +02:00
Antoine Bertin f2d2da94a1 Add the path to the repr of a ResultSubtitle 2012-06-24 15:43:44 +02:00
Antoine Bertin 14d19ff090 Fix subtitles being downloaded multiple times
This happened with the multi option because subtitles were
grouped by languages and Language('en-US') is different from
Language('en'). Now we take into account user's languages
preferred order
2012-06-24 15:43:09 +02:00
Antoine Bertin a547464d1e Add Chineese exception to TvSubtitles 2012-06-24 14:49:46 +02:00
Antoine Bertin 23b9aba560 Fix unicode representation of Video when it does not exist 2012-06-24 14:20:10 +02:00
Antoine Bertin 347038c528 Add test_videos to the main test suite 2012-06-24 13:53:41 +02:00
Antoine Bertin 2fc26d910a Use positional arguments for required fields of ResultSubtitle 2012-06-24 13:53:03 +02:00
Antoine Bertin 4828730ea3 Fix some encoding issues 2012-06-24 13:52:28 +02:00
Antoine Bertin 7c4f539a44 Use None as default for keywords in ResultSubtitle constructor 2012-06-24 13:49:13 +02:00
Antoine Bertin 598ef91a30 Do not convert to absolute paths in scan 2012-06-24 13:47:40 +02:00
Antoine Bertin 4862f12619 Update NEWS 2012-06-23 12:35:26 +02:00
Antoine Bertin c96ac214bb Update unittests 2012-06-23 11:24:00 +02:00
Antoine Bertin 8fb9cf6a0b Add __repr__ to Subtitles 2012-06-23 11:22:56 +02:00
Antoine Bertin 4a177b6008 Fix single download subtitles without the force option 2012-06-23 00:23:50 +02:00
Antoine Bertin 58b59a3304 Improve the download_zip_file method 2012-06-20 21:42:51 +02:00
Antoine Bertin 83e84a24b1 Always return the subtitle in Service.download 2012-06-20 21:18:54 +02:00
Antoine Bertin 322e6c1f1c Add Spanish (Latin America) exception to Addic7ed 2012-06-20 21:17:47 +02:00
Antoine Bertin d1ca77d7db Improve Addic7ed subtitles validation 2012-06-20 08:19:19 +02:00
Antoine Bertin d885c78b9a Fix group_by_video when a list entry has None as subtitles 2012-06-20 08:18:37 +02:00
Antoine Bertin 6c8a8a53e7 Avoid some other Addic7ed errors 2012-06-19 23:24:16 +02:00
Antoine Bertin 21ec9335fc Add support for Galician language in Subtitulos 2012-06-19 08:12:31 +02:00
Antoine Bertin 4c40a463da Add an integrity check after subtitles download for Addic7ed 2012-06-19 08:11:54 +02:00
Antoine Bertin 169e97975d Improve logging for file downloads 2012-06-19 08:11:06 +02:00
Antoine Bertin e26c65d4f1 Add error handling for if not strict in Language 2012-06-19 08:10:25 +02:00
Antoine Bertin d1dd86c825 Add possible filesizes for OpenSubtitles in unittests 2012-06-17 19:50:29 +02:00
Antoine Bertin e8388a757b Fix TheSubDB hash method to return None if the file is too small 2012-06-17 18:17:11 +02:00
Antoine Bertin 51c7d46390 Update services unittests
- Remove useless import
- Do not set verbosity
2012-06-17 12:26:16 +02:00
Antoine Bertin 84688acf32 Replace guessit.Language in Video.scan 2012-06-17 11:30:16 +02:00
Antoine Bertin f16ecd220a Fix language detection of subtitles 2012-06-17 11:28:32 +02:00
Antoine Bertin a0f89e46a8 Remove extra skip in unittests 2012-06-17 11:26:07 +02:00
Antoine Bertin 6fce503814 Merge branch 'develop' 2012-06-16 08:07:20 +02:00
Antoine Bertin e873a2fbd2 Put release date in NEWS 2012-06-16 08:06:03 +02:00
Antoine Bertin 53e15969c1 Merge branch 'develop' 2012-06-16 08:02:57 +02:00
Antoine Bertin c11d83a204 Update README 2012-06-16 00:28:05 +02:00
Antoine Bertin 35d664d414 Update documentation 2012-06-16 00:25:55 +02:00
Antoine Bertin 11a33e5425 Use travis-ci sidebar in the documentation 2012-06-16 00:25:43 +02:00
Antoine Bertin 97bd82967e Update diaoul-sphinx-themes 2012-06-16 00:24:30 +02:00
Antoine Bertin ca89339042 Fix CLI 2012-06-15 23:54:25 +02:00
Antoine Bertin 07054c3f68 Fix some naming mistakes 2012-06-15 22:56:11 +02:00
Antoine Bertin 72405f5c0e Add notifications and fix installation in travis-ci 2012-06-15 22:44:41 +02:00
Antoine Bertin d5232b8a68 Add the build status in README 2012-06-15 22:43:39 +02:00
Antoine Bertin d44d032e93 Update requirements
- Add EOL in requirements.txt
- Add optional-requirements.txt for lxml
2012-06-15 22:42:32 +02:00
Antoine Bertin d7122a9c98 Clean up
- Remove blank lines
- Move the strict test in Language
- Update comments in thesubdb
- Order language_map with codes first
2012-06-15 22:40:56 +02:00
Antoine Bertin eb0b9f29f2 Avoid ValueError when a new code is found 2012-06-15 22:38:44 +02:00
Antoine Bertin a8b7763a13 Call parent __init__ in Tasks 2012-06-15 22:37:51 +02:00
Antoine Bertin 1e7fe9d216 Fix encoding issues 2012-06-15 22:37:20 +02:00
Antoine Bertin a0dbfe8c4b Refactor unittests 2012-06-15 21:00:49 +02:00
Antoine Bertin ef2571d626 Fix return type of consume_task for DownloadTasks 2012-06-15 21:00:03 +02:00
Antoine Bertin d3cde7bd05 Update travis-ci file 2012-06-15 18:40:13 +02:00
Antoine Bertin 01191d632b Update NEWS 2012-06-15 18:32:25 +02:00
Antoine Bertin 5c934766f5 Clean up
- Improve imports
- Remove unused exceptions
- Remove blank lines
- Add __all__ in modules
2012-06-15 18:31:56 +02:00
Antoine Bertin decd0e2510 Use the same return type between list_sutbitles and download_subtitles 2012-06-15 17:54:22 +02:00
Antoine Bertin cd46dad14b Add unittests for api 2012-06-15 17:47:15 +02:00
Antoine Bertin 69c5075ced Fix returned results of download_subtitles in api 2012-06-15 17:46:44 +02:00
Antoine Bertin 4da9b7080d Update services and unittests 2012-06-15 17:00:45 +02:00
Antoine Bertin be112bc091 Remove special languages from OpenSubtitles list (und, mis and mul) 2012-06-15 08:21:20 +02:00
Antoine Bertin 8389c86ce7 Rename test_set to test_set_contains in language unittests 2012-06-15 01:07:58 +02:00
Antoine Bertin fa55d32563 Update addic7ed, bierdopje and opensubtitles 2012-06-15 01:07:32 +02:00
Antoine Bertin 185cc9844c Update api, async, core and services to use the new language module 2012-06-15 01:06:31 +02:00
Antoine Bertin 705fb0d342 Init the session in the constructor of ServiceBase 2012-06-15 01:04:17 +02:00
Antoine Bertin 2ce9ac0862 Compact cache functions in ServiceBase 2012-06-15 01:03:40 +02:00
Antoine Bertin a237cd856d Use the NotImplementedError in ServiceBase 2012-06-15 01:02:34 +02:00
Antoine Bertin b1e685ffc2 Make the Video hashable 2012-06-15 01:01:39 +02:00
Antoine Bertin d19dde9843 Replace guessit.language in subtitles module 2012-06-15 01:01:01 +02:00
Antoine Bertin 74693bf747 Add language_list class and its unittests 2012-06-15 00:59:16 +02:00
Antoine Bertin 530bc7f5ff Update language module documentation and improve exception messages 2012-06-15 00:58:16 +02:00
Antoine Bertin 1b660d1e6d Fix language_set substraction operation and add a unittest 2012-06-15 00:56:31 +02:00
Antoine Bertin 34ce0d640c Fix language_set constructor with list of tuples 2012-06-15 00:53:20 +02:00
Antoine Bertin ae2c08bfbd Remove languages for special situations from the list 2012-06-15 00:51:53 +02:00
Antoine Bertin f517a683e3 Merge branch 'develop' of github.com:Diaoul/subliminal into develop 2012-06-12 23:36:02 +02:00
Antoine Bertin b512439d41 Remove some blank lines 2012-06-12 23:33:04 +02:00
Antoine Bertin c62a2a7672 Order documentation by source 2012-06-12 23:32:51 +02:00
Antoine Bertin bcd5c8f610 Add language stuff 2012-06-12 23:32:25 +02:00
Antoine Bertin a75aff65d6 Use skip in unittests 2012-06-09 23:46:36 +02:00
Antoine Bertin 9500f882d5 Replace required_parsers with required_features
Remove detection code and rely on beautifulsoup4 for that
2012-06-09 23:45:27 +02:00
Antoine Bertin 7fc5f2ef97 Update requirements 2012-06-09 19:33:54 +02:00
Antoine Bertin 5d0808a1f9 Merge branch 'develop' of git@github.com:Diaoul/subliminal.git into develop 2012-06-09 11:26:28 +02:00
Antoine Bertin 511fe7410b Update README 2012-06-08 10:57:45 +03:00
Antoine Bertin 7ed6819b03 Update unittests 2012-06-07 21:58:55 +02:00
Antoine Bertin 4709e26bd5 Update Podnapisi 2012-06-07 21:57:46 +02:00
Antoine Bertin 6e1fa561f0 Remove unused stuff in unittests 2012-06-07 18:11:09 +02:00
Antoine Bertin d7668d3573 Fix Podnapisi query method 2012-06-07 18:10:55 +02:00
Antoine Bertin 410295486a Update documentation 2012-06-07 18:09:30 +02:00
Antoine Bertin 0d63b47560 Clean up unused stuff in unittests 2012-06-03 22:05:41 +02:00
Antoine Bertin d3cb956061 Use the pythonic syntax for not in list 2012-06-03 22:04:54 +02:00
Antoine Bertin 2e00accfab Add Podnapisi service 2012-06-03 22:04:15 +02:00
Antoine Bertin c54a60097c Fix requirements 2012-06-03 15:02:23 +02:00
Antoine Bertin 58a54bce06 Merge pull request #84 from wackou/develop
Fixes for TvSubtitles and OpenSubtitles services
2012-06-03 05:15:32 -07:00
Antoine Bertin d00e6905e1 Fix python 2.6 compatibility
list.copy() is 2.7+
2012-06-03 14:10:36 +02:00
Nicolas Wack c7388c9247 Updated requirements for GuessIt 2012-05-12 17:30:30 +02:00
Nicolas Wack 5a2ab412b8 Fixed TvSubtitles download (we get back zip files, not srt ones) 2012-05-12 16:37:25 +02:00
Nicolas Wack 584acb0856 Updated OpenSubtitles' language support 2012-05-12 16:18:07 +02:00
Nicolas Wack 122f41507a Service.check_validity requires languages to be a set 2012-05-09 01:33:22 +02:00
Antoine Bertin 6a78564460 Add optional requirements to travis-ci configuration 2012-05-06 15:59:26 +02:00
Antoine Bertin 096cd5e09c Rename parser related variables. Clean up 2012-05-06 15:51:24 +02:00
Antoine Bertin cc3fa4b11a Update requirements 2012-05-06 12:08:34 +02:00
Antoine Bertin f050687487 Add automatic parser detection for BeautifulSoup 2012-05-06 11:40:12 +02:00
Antoine Bertin 4165ed0b9e Bump version to 0.6 in services 2012-05-06 11:39:23 +02:00
Antoine Bertin 3032590b8e Reorder imports 2012-05-06 11:38:36 +02:00
Antoine Bertin 113b504057 Use unicode in logging messages 2012-05-06 11:36:53 +02:00
Antoine Bertin 1a49f0b3ab Update documentation to remove references to subliminal.languages 2012-05-05 23:30:03 +02:00
Antoine Bertin a79143644b Fix import errors and be pep8 compliant 2012-05-05 23:12:02 +02:00
Antoine Bertin a2559d2d31 Add tests to setup.py and fix travis-ci support
Run tests with python setup.py test
2012-05-05 20:25:00 +02:00
Antoine Bertin 057933d737 Add support for travis-ci 2012-05-05 20:01:45 +02:00
Antoine Bertin bb16df5770 Change message when CLI does not download subtitles 2012-05-05 19:46:45 +02:00
Antoine Bertin b9c8ac23cc Fix requirements 2012-05-05 19:42:15 +02:00
Antoine Bertin bad6f77a01 Merge branch 'develop' of https://github.com/wackou/subliminal into develop 2012-05-05 19:42:07 +02:00
Nicolas Wack bb28945eba Optimized scanning for subtitles 2012-05-04 00:50:59 +02:00
Nicolas Wack 5a6ed82c5f Fixed possibly too greedy regexp 2012-05-03 23:10:21 +02:00
Nicolas Wack 055b2c7139 Fixed error message when no subtitles can be found in zipfile 2012-05-03 22:58:37 +02:00
Nicolas Wack e049eebeb0 Updated requirements for GuessIt 2012-04-28 20:56:33 +02:00
Nicolas Wack 0f3c68b7ef Removed bs4wrapper (requires BeautifulSoup>=4 now) 2012-04-28 20:23:39 +02:00
Nicolas Wack 607efff342 Merge remote-tracking branch 'olifozzy/develop' into languages_refactor 2012-04-25 02:14:14 +02:00
Nicolas Wack a88c05f7c3 Finished switching everything to guessit.Language; removed old language helper functions 2012-04-25 02:05:43 +02:00
Nicolas Wack 3aabbfde4c Subliminal now uses guessit.Language internally mostly everywhere 2012-04-24 22:25:21 +02:00
Nicolas Wack b447825a42 More flexilbe language handling 2012-04-22 03:28:12 +02:00
Nicolas Wack 2a1fb7bcb7 Unittests now clean up after themselves 2012-04-22 01:28:37 +02:00
Nicolas Wack fa7211c8b6 Added check on the size of the downloaded subtitle to make sure it is correct 2012-04-22 01:15:43 +02:00
Nicolas Wack bfa961a005 added unittests for the caching system 2012-04-21 21:43:08 +02:00
Nicolas Wack 4d9855d91c Refactored a bit unittests to use the generic EpisodeServiceTestCase 2012-04-21 20:09:04 +02:00
Nicolas Wack b09c069af8 Changed order of parameters for BierDopje.query() so that it follows the same order as the other services 2012-04-21 19:27:09 +02:00
Antoine Bertin 8fdb241a72 Update documentation 2012-04-15 23:25:14 +02:00
Nicolas Wack de781d0eec Refactored a bit the service unittests to avoid code duplication 2012-04-13 01:56:49 +02:00
Antoine Bertin 2959f52099 Fix CLI when --age is not given 2012-04-12 16:01:09 +03:00
Antoine Bertin 1e11c02006 Fix requirements for python 2.7
Add a requirement exception for previous versions of python
2012-04-11 22:24:37 +02:00
Antoine Bertin 12a58b0ca3 Bump version 2012-04-11 22:20:53 +02:00
Antoine Bertin 27f521d74e Update documentation 2012-04-11 21:15:02 +02:00
Antoine Bertin bbc3472873 Fix description for --age option in CLI 2012-04-11 20:35:47 +02:00
Antoine Bertin 680c699dfa Add --age option in CLI 2012-04-11 20:28:48 +02:00
Antoine Bertin b7ba6d51e2 Fix --workers option in CLI 2012-04-11 20:28:34 +02:00
Nicolas Wack 1f2128999f Correctly set config from a task to its cached service before consuming said task 2012-04-11 20:19:45 +02:00
olifozzy f03503b7f8 Change copyright 2012-04-11 09:53:00 +02:00
Antoine Bertin ae1b86173e Fix missing scan_filter option in async 2012-04-11 00:09:17 +02:00
Antoine Bertin e783c255b7 Add scan_filter option to filter out some paths 2012-04-10 23:48:49 +02:00
Nicolas Wack 0f86d7321f reworked caching system so that each ServiceConfig has its own Cache instance 2012-04-10 23:45:31 +02:00
Antoine Bertin f085b0fc8e Add argparse to the requirements (for python 2.6) 2012-04-10 22:07:57 +02:00
Nicolas Wack b4beba284d Caching system now has a separate cache and cache file for each service 2012-04-09 21:10:09 +02:00
Nicolas Wack c1e70e9e21 added TvSubtitles to the list of services in subliminal/core.py 2012-04-09 19:29:20 +02:00
olifozzy fca2c698fc Add unit tests 2012-04-06 15:09:35 +02:00
olifozzy 5d5b23f907 Change copyright author 2012-04-06 09:37:55 +02:00
olifozzy 2860e58830 Add Addic7ed service. 2012-04-06 01:06:26 +02:00
Antoine Bertin e8dec6c143 Merge branch 'develop' 2012-03-25 18:01:53 +02:00
Antoine Bertin 8b2b19de4b Update and fix NEWS 2012-03-25 17:58:42 +02:00
Antoine Bertin af162578c0 Merge branch 'develop' 2012-03-25 17:47:15 +02:00
Antoine Bertin c6ccfa4417 Bump version 2012-03-25 17:46:56 +02:00
Antoine Bertin 132001a1be Improve error handling of enzyme parsing 2012-03-25 17:46:10 +02:00
Antoine Bertin 830c09dde7 Merge branch 'develop' 2012-03-25 13:12:56 +02:00
Antoine Bertin 57bcde2511 Add missing setup.cfg 2012-03-25 13:12:46 +02:00
Antoine Bertin c1db5b43e5 Merge branch 'develop' 2012-03-25 12:50:27 +02:00
Antoine Bertin 7b7f29720c Update README 2012-03-25 12:10:55 +02:00
Antoine Bertin 13320db360 Clean up documentation 2012-03-25 12:10:46 +02:00
Antoine Bertin dd9d63d74e Remove version of requests in requirements.txt 2012-03-25 12:05:35 +02:00
Antoine Bertin 739ed2be6d Update NEWS 2012-03-25 12:04:36 +02:00
Antoine Bertin 95d9ed685f Use unicode for release of ResultSubtitle 2012-03-25 11:53:25 +02:00
Antoine Bertin 1064d60873 Fix setup sdist including everything. Improve keywords and requirements 2012-03-25 10:39:11 +02:00
Nicolas Wack 014c3249b5 Much better TvSubtitles service; uses cache now too 2012-03-21 20:14:24 +01:00
Nicolas Wack 59920a5537 Enhanced caching system 2012-03-21 16:27:26 +01:00
Nicolas Wack 8e04de4d65 Better BeautifulSoup4 wrapper 2012-03-20 20:31:54 +01:00
Nicolas Wack 3ff8ebea7d fixed some python 2.6 issues
some objects can be used in 2.7 as context managers but not in 2.6
2012-03-15 22:33:35 +01:00
Nicolas Wack 29096d6700 Merge branch 'develop' of github.com:wackou/subliminal into develop 2012-03-14 23:48:25 +01:00
Nicolas Wack 1c59fc829f added first version of a TvSubtitles service 2012-03-14 23:42:43 +01:00
Nicolas Wack ca5c0427b8 allow services to download an extract zip files 2012-03-14 22:30:38 +01:00
Nicolas Wack f1dd77bdfd Compatibility for BeautifulSoup 3 and 4
BeautifulSoup4 is imported by default if present.
2012-03-13 01:54:33 +01:00
Antoine Bertin a93f74c3ec Do not create download tasks without subtitles 2012-03-10 01:43:13 +01:00
Antoine Bertin a5b9d2dbd6 Add missing documentation stuff 2012-03-10 01:12:51 +01:00
Antoine Bertin 2e6e27991a Use modified flask's sphinx documentation theme 2012-03-10 00:50:02 +01:00
Antoine Bertin b0ae70a88f More documentation 2012-03-10 00:19:22 +01:00
Antoine Bertin af2e8ba7bd Fix wrong __all__ in utils 2012-03-10 00:04:37 +01:00
Antoine Bertin 15ca8dac1d More documentation 2012-03-10 00:04:19 +01:00
Antoine Bertin b6f6d17daa More documentation 2012-03-07 08:37:41 +01:00
Nicolas Wack 4d06316d22 ported services to BeautifulSoup4 2012-03-05 22:02:31 +01:00
Antoine Bertin 9290d7e96f Change list_subtitles so it returns a dict of subtitles by video 2012-03-05 21:46:53 +01:00
Antoine Bertin 10f931ab68 Add some core components to subliminal 2012-03-05 21:45:09 +01:00
Antoine Bertin 75986be154 Add some examples to the documentation 2012-03-03 23:19:09 +01:00
Antoine Bertin 1b53c9377d Remove old stuff in README 2012-03-03 23:18:58 +01:00
Antoine Bertin 7b31c000cd Allow zero depth entries 2012-03-03 22:37:31 +01:00
Antoine Bertin d180dd5f6a Fix subswiki and subtitulos 2012-03-03 22:24:34 +01:00
Antoine Bertin b7d4d67bec Add docstrings to async 2012-03-03 18:57:45 +01:00
Antoine Bertin 8a018ed3a8 Update async unittests 2012-03-03 18:27:42 +01:00
Antoine Bertin fe41ab9c6b Rename attribute _workers to workers in Pool 2012-03-03 18:27:23 +01:00
Antoine Bertin 7cf00b4b58 Add content to NEWS 2012-03-03 15:23:10 +01:00
Antoine Bertin 224b2399bc Fix COPYING 2012-03-03 15:22:51 +01:00
Antoine Bertin bef04df3b3 Fix CLI 2012-03-03 10:56:47 +01:00
Antoine Bertin a956707669 Fix async 2012-03-03 10:56:42 +01:00
Antoine Bertin d812af8535 Add a unittest for async 2012-03-02 11:02:47 +01:00
Antoine Bertin 0edb45d851 Add Pool to subliminal 2012-03-02 11:01:44 +01:00
Antoine Bertin e25903f282 Fix services 2012-03-02 10:43:21 +01:00
Antoine Bertin 591f98957f Fix async 2012-03-02 10:43:07 +01:00
Antoine Bertin 01eb6a7a5a Start working on the CLI 2012-03-02 10:42:51 +01:00
Antoine Bertin 2e9fc52ba2 Update unittests 2012-03-01 22:07:53 +01:00
Antoine Bertin 25c5b0f695 Update User-Agent 2012-03-01 19:30:05 +01:00
Antoine Bertin 88b242fe65 Add __version__ to subliminal 2012-03-01 18:00:26 +01:00
Antoine Bertin 28b7414347 Rename Subliminal to subliminal 2012-03-01 18:00:08 +01:00
Antoine Bertin 71e58336aa Remove plugins 2012-03-01 17:48:20 +01:00
Antoine Bertin b3388cfef9 Update unittests 2012-03-01 17:43:31 +01:00
Antoine Bertin ad1cbc8318 Add subswiki, subtitulos and thesubdb services 2012-03-01 17:43:18 +01:00
Antoine Bertin 6d47022bb5 Use new check_validity method in services 2012-03-01 17:42:51 +01:00
Antoine Bertin 4e96c77d2b Add a check_validity method to ServiceBase 2012-03-01 17:42:04 +01:00
Antoine Bertin b1071e0870 Add pass to some ServiceBase methods 2012-03-01 17:40:37 +01:00
Antoine Bertin e786c456af Default method for Service download 2012-03-01 17:40:05 +01:00
Antoine Bertin 68fb07ee0d Remove unused test files 2012-03-01 17:35:09 +01:00
Antoine Bertin 35bf0b803c Remove unused exception 2012-03-01 13:12:13 +01:00
Antoine Bertin ef1a142c59 Clean imports in core 2012-03-01 13:12:02 +01:00
Antoine Bertin 14ff363ce6 Replace plugin with service 2012-03-01 13:11:47 +01:00
Antoine Bertin 7872bca40d Fix very long lines in videos 2012-03-01 13:09:04 +01:00
Antoine Bertin 4b4272ce85 Improve setup.py stuff 2012-03-01 13:07:34 +01:00
Antoine Bertin 28b99ad374 General refactoring
- Move from md to rst
- Add sphinx docstrings
- Rename plugins to services
- Replace Subliminal class with the async module
- Straightforward to use
- Use relative imports
- Remove mkvmerge stuff
2012-02-29 22:28:54 +01:00
Antoine Bertin b7fb9a7703 Rename test to tests 2012-02-27 19:11:58 +01:00
Antoine Bertin 3eb4913863 Merge branch 'develop' of github.com:Diaoul/subliminal into develop 2012-02-27 16:46:42 +01:00
Antoine Bertin fe46afd571 Update license and minor changes 2012-02-27 15:34:23 +01:00
Antoine Bertin e8ba65ef22 Use MATCHING_CONFIDENCE 2012-01-24 15:25:27 +01:00
Antoine Bertin 4854b2630e Fix CLI 2012-01-11 13:21:04 +01:00
Antoine Bertin e7d3465cdd Update required packages 2012-01-02 07:56:06 +01:00
Antoine Bertin b04975af2e Improve __init__ 2012-01-02 07:56:06 +01:00
Antoine Bertin 947384aa6f Use relative imports 2012-01-02 07:56:06 +01:00
Antoine Bertin 8db65824bb Merge pull request #54 from bonega/develop
Fix for "ValueError: zero length field name in format"
2011-12-20 12:12:19 -08:00
Andreas Liljeqvist ec1c625a1a Fix for "ValueError: zero length field name in format"
Python 2.6 must have positional arguments for format.
2011-12-20 20:21:02 +01:00
Antoine Bertin ded2171375 Update README 2011-12-10 10:47:17 +01:00
Antoine Bertin fdc388a117 Fix __all__ of core.py 2011-12-08 16:02:31 +01:00
Antoine Bertin 2320cfd8e5 Fix a bug in matching_confidence 2011-12-08 15:44:23 +01:00
Antoine Bertin 1be88b8454 Fix variable name conflict 2011-12-08 15:43:35 +01:00
Antoine Bertin e3ea8ee06b Fix variable name conflicts 2011-12-08 15:17:14 +01:00
Antoine Bertin d7c10ae1b8 Code clean-up 2011-12-08 15:13:54 +01:00
Antoine Bertin 03f209b2b0 Use enzyme 2011-12-04 22:36:04 +01:00
Antoine Bertin c0ddac5eed Update license 2011-12-04 22:29:29 +01:00
Antoine Bertin 0e06088b3e Update video extensions 2011-12-02 10:53:19 +01:00
Antoine Bertin 75de6bbb3d Add more video extensions 2011-11-29 14:34:15 +01:00
Antoine Bertin 0e0189d518 Fix language detection for EmbeddedSubtitles 2011-11-24 00:06:09 +01:00
Antoine Bertin c166bfd334 Update unittest 2011-11-21 21:23:59 +01:00
Antoine Bertin 496cb3bc68 Fix mkvmerge 2011-11-21 21:23:34 +01:00
Antoine Bertin d5176b1b48 Use langcode instead of language from kaa.metadata 2011-11-17 22:19:53 +01:00
Antoine Bertin 2560c5f711 Fix fromKaa constructor 2011-11-17 08:58:34 +01:00
Antoine Bertin 062fcae014 Use utf-8 encoded strings for comparison 2011-11-17 08:58:15 +01:00
Antoine Bertin 90e6a4b94a Fix core.py for single srt detection 2011-11-17 08:57:57 +01:00
Antoine Bertin c7bf7dcb45 Fix CLI 2011-11-17 08:57:41 +01:00
Antoine Bertin 069ab7405f Delete api.py for now 2011-11-17 08:57:33 +01:00
Antoine Bertin ed2e8aa510 Fix setup.py 2011-11-16 22:46:46 +01:00
Antoine Bertin 08d7ce7a2b Fix missing import 2011-11-14 18:03:07 +01:00
Antoine Bertin 8dabb8cbbd Fix typo in scan 2011-11-11 21:46:44 +01:00
Antoine Bertin 3cf265f6c9 Change version from 1.0 to 0.4 2011-11-11 20:37:19 +01:00
Antoine Bertin 1b8dd8cc83 Update gitignore 2011-11-11 20:37:03 +01:00
Antoine Bertin d12226eb7e Change version number from 1.1 to 0.5 2011-11-11 20:32:45 +01:00
Antoine Bertin 6d9c5c34f2 Update unittests 2011-11-11 20:23:04 +01:00
Antoine Bertin 251b1fb446 Multiple enhancements
- Add languages.py with language related stuff
- Add multiple Subtitle classes
- Update scan so it returns [(video, [subtitle])]
- Update mkvmerge to support embedded subtitles
- Replace pt-br with (Brazilian, po, pob)
2011-11-11 20:21:06 +01:00
Antoine Bertin f39df2ff46 Update Video and Subtitle. New scan method with kaa.metadata 2011-11-08 01:35:07 +01:00
Antoine Bertin 945bcee3f5 Do not print Popen result in console 2011-11-08 01:33:04 +01:00
Antoine Bertin fef5c09a76 Add mapping for ISO-639-2 languages 2011-11-08 01:31:53 +01:00
Antoine Bertin 54a9851dee Fix mkvmerge 2011-11-06 22:17:49 +01:00
Antoine Bertin b609659f65 Add more logging when skipping videos 2011-11-06 13:55:55 +01:00
Antoine Bertin a352ecc771 Init requests session in PluginBase 2011-11-06 11:27:04 +01:00
Antoine Bertin abbf3bc20f Add a compatibility option to CLI 2011-11-05 23:19:30 +01:00
Antoine Bertin 21704696da Remove unused stuff 2011-11-05 21:42:39 +01:00
Antoine Bertin ffa38694df Add pt-br in valid languages 2011-11-05 21:42:23 +01:00
Antoine Bertin 090bdbeccd Fix "long int exceeds XML-RPC limits" in OpenSubtitles 2011-11-05 21:31:21 +01:00
Antoine Bertin 7f0d9fea54 Update unittests 2011-11-05 21:07:25 +01:00
Antoine Bertin 8f6839dfe8 Add keywords in Subtitulos subtitles 2011-11-05 21:04:27 +01:00
Antoine Bertin 246d35fdb1 Fix Subtitulos list 2011-11-05 21:04:11 +01:00
Antoine Bertin 11b0037986 Fix Subtitulos require_video 2011-11-05 21:03:48 +01:00
Antoine Bertin ab34a23bb4 Fix SubsWiki download 2011-11-05 21:03:34 +01:00
Antoine Bertin 288b9f00e8 Add parameters check in SubsWiki 2011-11-05 21:03:15 +01:00
Antoine Bertin 712842a235 Fix SubsWiki 2011-11-05 21:02:49 +01:00
Antoine Bertin 82b6b3faf5 Fix SubsWiki require_video 2011-11-05 21:02:22 +01:00
Antoine Bertin 563d0f7566 Add parameters check in BierDopje 2011-11-05 21:02:10 +01:00
Antoine Bertin a9c605f7c9 Update OpenSubtitles 2011-11-05 21:01:52 +01:00
Antoine Bertin 21a1c1243b Add Subtitulos to the list 2011-11-05 20:59:07 +01:00
Antoine Bertin 7c94f1792b Use requests in downloadFile 2011-11-05 17:37:00 +01:00
Antoine Bertin b0f87f4e37 Fix scan 2011-11-05 17:36:40 +01:00
Antoine Bertin 4bb10cf89a Fix OpenSubtitles terminate when could not LogIn 2011-11-03 08:50:06 +01:00
Antoine Bertin da22709623 Fix downloadSubtitles 2011-11-03 08:49:29 +01:00
Antoine Bertin 7c8e0457ce Update unittests 2011-11-02 08:57:17 +01:00
Antoine Bertin f18656738d Do not accept Movie in Subtitulos 2011-11-02 08:56:44 +01:00
Antoine Bertin 517cf6205c Add Subtitulos 2011-11-02 08:56:15 +01:00
Antoine Bertin f45f0b492d Fix SubsWiki list 2011-11-02 08:56:04 +01:00
Antoine Bertin 2271d3d4c5 Update unittests 2011-11-02 08:26:37 +01:00
Antoine Bertin b5293e4455 Add SubsWiki 2011-11-02 08:26:12 +01:00
Antoine Bertin c6614e2faa Fix split_keyword 2011-11-02 08:25:48 +01:00
Antoine Bertin b8ddc72fe8 Add other plugins to Subliminal 2011-11-02 08:25:19 +01:00
Antoine Bertin 3e63414b80 Various little changes 2011-11-02 08:25:01 +01:00
Antoine Bertin 58338fc946 Use pt-br in OpenSubtitles 2011-11-02 08:24:19 +01:00
Antoine Bertin acba7d1228 Update unittests for TheSubDB 2011-10-31 22:12:05 +01:00
Antoine Bertin 956d8ef0d8 Update unittests 2011-10-31 22:11:07 +01:00
Antoine Bertin 7bd6db0c5d Add TheSubDB 2011-10-31 22:09:56 +01:00
Antoine Bertin a291af863c Fix GetSubtitle 2011-10-31 22:09:43 +01:00
Antoine Bertin 7f828783a7 Improve HTTP error handling 2011-10-31 22:09:22 +01:00
Antoine Bertin eb57a83bf5 More accurate logging 2011-10-31 22:08:34 +01:00
Antoine Bertin 9720679461 More logging 2011-10-27 01:13:52 +02:00
Antoine Bertin cc10d69b47 Add structure for subtitles.com.br provider 2011-10-27 01:13:35 +02:00
Antoine Bertin e5b8b2fc8e Update unittests 2011-10-27 01:12:29 +02:00
Antoine Bertin b3880b3ec8 Update unittests 2011-10-26 19:28:27 +02:00
Antoine Bertin eaafa7fb25 Fix BierDopje query 2011-10-26 19:27:02 +02:00
Antoine Bertin be346af2ba Fix OpenSubtitles terminate 2011-10-26 19:26:45 +02:00
Antoine Bertin a5d53eaeba Add BierDopje plugin 2011-10-26 08:09:27 +02:00
Antoine Bertin af340eec7b Update CLI 2011-10-25 08:43:01 +02:00
Antoine Bertin 0baac5d829 New setup stuff
- Add requests to make HTTP requests
- Add suds to make SOAP requests
2011-10-25 08:42:43 +02:00
Antoine Bertin 1631d48490 Docstring in subtitles 2011-10-25 08:41:38 +02:00
Antoine Bertin f9874c3c37 Use only one plugin instance within the same worker. Other stuff
- More docstrings
- stopWorkers is more flexible
- pauseWorkers is now just a wrapper
- auto arg for list and download default to False
because of the new with statement
2011-10-25 08:41:16 +02:00
Antoine Bertin 2f8e49b8df Fix bad import. New stuff in __init__ 2011-10-25 08:38:11 +02:00
Antoine Bertin 8455b2c480 New plugins organization
- One file only to avoid importing parent
- Remove shared data and use single plugin instance instead
2011-10-25 08:37:40 +02:00
Antoine Bertin fa25502803 Remove broken plugins from the list 2011-10-25 08:09:08 +02:00
Antoine Bertin bfaa45252a Add plugins and languages args to constructor 2011-10-25 08:07:42 +02:00
Antoine Bertin 9af3627b62 Update OpenSubtitles 2011-10-24 08:46:06 +02:00
Antoine Bertin 8edccedbbd Fix OpenSubtitles when no data is returned 2011-10-24 08:45:49 +02:00
Antoine Bertin 8d020fd39a Update abstract methods in PluginBase 2011-10-24 08:45:23 +02:00
Antoine Bertin 139314eaba Remove useless stuff in PluginBase 2011-10-24 08:44:56 +02:00
Antoine Bertin 9cbd5ccf05 Improve __init__ in PluginBase 2011-10-24 08:42:48 +02:00
Antoine Bertin 314cab60aa Fix wrong state in pauseWorkers 2011-10-24 08:42:07 +02:00
Antoine Bertin 025b53092e Improve shared data 2011-10-24 08:41:48 +02:00
Antoine Bertin 7a0ea2bdff Implement with statement in Subliminal 2011-10-24 08:41:26 +02:00
Antoine Bertin 0a79820dd6 Improve __init__ 2011-10-24 08:39:35 +02:00
Antoine Bertin 7203ad93e4 Fix error in logging when video has no path 2011-10-24 08:39:09 +02:00
Antoine Bertin 504dda817a Put workers in property to raise BadStateError 2011-10-24 08:38:24 +02:00
Antoine Bertin 1069cff6cf Fix DownloadTask put in the taskQueue 2011-10-24 08:36:35 +02:00
Antoine Bertin 2662150710 Fix CLI 2011-10-24 08:34:58 +02:00
Antoine Bertin ad7c187201 Update OpenSubtitles 2011-10-22 18:26:30 +02:00
Antoine Bertin d7125413e5 Fix get_subtitle_path 2011-10-22 18:26:01 +02:00
Antoine Bertin 87d2d9e1f2 Init keywords with empty set in Subtitle 2011-10-22 17:57:49 +02:00
Antoine Bertin 9d712b86fa Improve the use of shared data
- Plugins have now access to their shared data only
- Add a class attribute shared_support in plugins
2011-10-22 17:57:17 +02:00
Antoine Bertin 69baf1e039 Add a video attribute to DownloadTask 2011-10-22 17:49:34 +02:00
Antoine Bertin 906707f0e0 Add size attribute and init hashes in constructor for Video 2011-10-22 17:48:28 +02:00
Antoine Bertin 3762ab1878 Review the factory method for videos 2011-10-22 17:46:14 +02:00
Antoine Bertin 12d7fe7ebf Fix a bug in scan 2011-10-22 17:45:26 +02:00
Antoine Bertin 5a6c278167 Finalize new structure
- Rename files_mode to filemode
- New PluginConfig object to give configuration to plugins
- Improve subtitles ordering with matching_confidence method
- Share plugins data inside a same worker to improve performances
- Fix get_subtitle_path
- Remove KEYWORD_SEPARATORS
- Replace keywords by guess in videos
- New get_keywords(guess) method
2011-10-22 15:21:00 +02:00
Antoine Bertin bddfa15eb8 Working OpenSubtitles 2011-10-20 08:50:21 +02:00
Antoine Bertin 899de2ebe3 More work on 1.1
- Introduce the sort_order so users can specify in which order
subtitles are downloaded (prefer language over plugin, etc.)
- Use key function in sorting
- List method now returns a list of tuples: [(video, [subtitle])]
- OpenSubtitles refactoring
- get_subtitle_path method in subtitles
- imdbid in Video
2011-10-19 00:19:42 +02:00
Antoine Bertin eef578b939 More work on 1.1
- Lots of #TODO
- Differentiate missing language and invalid language exceptions
- Extensions for videos and subtitles
- Mimetypes in videos
- Review PluginBase: use more classmethods
- Add mkvmerge method in videos
- Update unittests
- New utils.py for commonly used functions
2011-10-17 00:55:00 +02:00
Antoine Bertin a7608171b1 Update .gitignore 2011-10-17 00:49:19 +02:00
Antoine Bertin f3292c2916 Release v1.0b6 2011-10-08 19:44:14 +02:00
Antoine Bertin 048d6adfa3 Exit the plugin if no correct language is given 2011-10-08 19:41:04 +02:00
Antoine Bertin f004271e8b Fix log message in PluginBase 2011-10-08 19:37:33 +02:00
Antoine Bertin 63ef2f1273 Release v1.0b5 2011-10-08 19:03:51 +02:00
Antoine Bertin 4d3b050a81 Changes for 1.1
- Review code organization
- Add Video classes
- Improve Subtitle class
- Use classmethods
- Remove all socket.setdefaulttimeout
- Replace xmlrpclib.Server (deprecated) by xmlrpclib.ServerProxy
2011-10-08 18:52:36 +02:00
Antoine Bertin f7f2b80028 Update unittest 2011-10-07 19:22:45 +02:00
Antoine Bertin ec21d564b2 Handle correctly non existent file paths 2011-10-07 19:13:21 +02:00
Antoine Bertin 6d2b772f21 Add a timeout on downloadFile method so it does not get stuck 2011-10-07 19:11:05 +02:00
Antoine Bertin d506dd2095 Remove useless checkLanguages method 2011-10-07 19:10:11 +02:00
Antoine Bertin c66028793d Release v1.0b4 2011-10-07 13:59:37 +02:00
Antoine Bertin 0e3467d3aa Add possible_languages method to filter undesired languages 2011-10-07 01:35:34 +02:00
Antoine Bertin d9dc6832e3 Be more pythonic
- Use property decorators
- Use new-style classes
2011-10-07 01:33:28 +02:00
Antoine Bertin a57d49abf1 Remove useless comments 2011-10-07 01:31:50 +02:00
Antoine Bertin 614388e28f Fix bad error logging when there is no error 2011-10-07 01:31:02 +02:00
Antoine Bertin 7e1a09fb64 Do not forget to remove the gziped file in OpenSubtitles 2011-10-07 01:30:02 +02:00
Antoine Bertin 6efbbd9cad Use logging to print stack instead of traceback 2011-10-07 00:00:07 +02:00
Antoine Bertin e0b5894e3f Use sets in listSubtitles for wanted languages 2011-10-06 23:59:03 +02:00
Antoine Bertin 5ed153425b Preserve order for languages and plugins 2011-10-06 23:56:09 +02:00
Antoine Bertin c961c9be93 Fix multiple issues
- Add a DownloadFailedError exception
- Fix not working "skip on failure" on a download task
- Add extensions to detect subtitles files
- Replace recursiveSearch with scan method
- Fix 'list' object has no attribute 'path'
2011-10-05 22:30:33 +02:00
Antoine Bertin 0bafc9d31f Remove Addic7ed from plugins (not working anymore) 2011-10-05 21:31:54 +02:00
Antoine Bertin a48cd6c824 Remove duplicates in setters for languages and plugins 2011-10-02 21:23:46 +02:00
Antoine Bertin cb899b81e4 Release v1.0b3 2011-10-01 17:02:35 +02:00
Antoine Bertin 918151013e Improve error handling in subliminal 2011-10-01 17:01:54 +02:00
Antoine Bertin 4cb9ebb5e3 Fix search with special characters in some plugins 2011-10-01 15:06:46 +02:00
Antoine Bertin e14a5f1776 Improve error handling in plugins 2011-10-01 15:04:21 +02:00
Antoine Bertin 2437dc602c More logging in plugins 2011-10-01 15:02:38 +02:00
Antoine Bertin 8962d84ed3 Release v1.0b2 2011-09-29 23:55:58 +02:00
Antoine Bertin 4172d2a8b1 Change the Subtitle class. Make download return a list of Subtitles 2011-09-29 23:55:33 +02:00
Antoine Bertin 78a2166efe Release v1.0b 2011-09-29 23:51:30 +02:00
Antoine Bertin 20a2519d92 Fix unittest 2011-09-28 21:54:45 +02:00
Antoine Bertin a7da526885 Compare lowercase only in OpenSubtitles 2011-09-28 21:53:18 +02:00
Antoine Bertin d2010c3510 Log a warning if non unicode is submitted 2011-09-28 21:40:38 +02:00
Antoine Bertin 14c0c6fe6b Fix possible encoding issues in logger using repr instead of str 2011-09-28 21:39:48 +02:00
Antoine Bertin 5d8be66167 Add docstrings in classes 2011-09-28 21:37:37 +02:00
Antoine Bertin 9ac838a257 Make cmpSubtitles and recursiveSearch public 2011-09-27 21:38:30 +02:00
Antoine Bertin 0cd1016cae Use two result queues instead of one 2011-09-27 21:35:14 +02:00
Antoine Bertin 49b8486f44 Use simple quotes 2011-09-27 21:26:27 +02:00
Antoine Bertin 0a363ba5d5 Add a method to add tasks with default priority 2011-09-27 21:24:55 +02:00
Antoine Bertin 5fbfaef31d Fix BadStateError in CLI 2011-09-26 21:41:44 +02:00
Antoine Bertin 9324fc4c07 Add auto argument to manage workers 2011-09-26 21:41:20 +02:00
Antoine Bertin 171147dfcd Fix bad argument in unittest 2011-09-26 21:39:04 +02:00
Antoine Bertin dee0b33cec Fix missing import 2011-09-26 21:37:52 +02:00
Antoine Bertin f9c7654b7c Update unittest 2011-09-24 16:51:37 +02:00
Antoine Bertin b1e0371bb5 Use a PriorityQueue for tasks 2011-09-24 16:50:55 +02:00
Antoine Bertin f94957c68d Accept basestring input, not just unicode 2011-09-24 16:48:16 +02:00
Antoine Bertin 03dd15f718 Merge pull request #15 from abenea/master
Bugfixes
2011-09-23 15:08:44 -07:00
Andrei Benea 938b30604b Fix --cache_dir. 2011-09-23 21:25:28 +03:00
Andrei Benea acff303897 Fix "Downloaded subtitles" report. 2011-09-23 20:46:37 +03:00
Andrei Benea 978f4fb376 Fix exception in _recursiveSearch if no languages are selected. 2011-09-23 20:39:50 +03:00
Andrei Benea 50e0a6704b Add missing newline. 2011-09-23 20:39:08 +03:00
Andrei Benea b4e9c0e655 Don't hang on exceptions from the main thread. 2011-09-23 20:11:24 +03:00
Antoine Bertin 97ab555e2d Update unittest 2011-09-01 21:35:24 +02:00
Antoine Bertin 9bc12ec619 Fix BierDopje download 2011-09-01 21:35:04 +02:00
Antoine Bertin e4c12dfe5c Remove unused imports and variables 2011-09-01 08:38:53 +02:00
Antoine Bertin ca0d8e4ab1 Update .gitignore 2011-09-01 08:37:54 +02:00
Antoine Bertin 570dc7ef9a Remove last encodingKludge import 2011-08-31 21:38:04 +02:00
Antoine Bertin 3e7cac972d Update unittests 2011-08-29 22:23:24 +02:00
Antoine Bertin 9c7ac431b5 Update docstrings. Double quotes to simple quotes. Minor fixes
Minor fixes includes:
- getClassName() converted to self.__class__.__name__
- Typos
- listTeams separators in plugins to include more separators
2011-08-29 21:36:49 +02:00
Antoine Bertin 8f50b317cc Remove encodingKludge 2011-08-29 21:28:35 +02:00
Antoine Bertin c5e2e8c4b0 Fix wrong subliminal version in TheSubDB 2011-08-29 21:22:58 +02:00
Antoine Bertin 7aec1f7653 Remove unused multi_languages_queries in plugins 2011-08-29 21:22:30 +02:00
Antoine Bertin 65a677e6d4 Remove unused plugins_config 2011-08-29 21:20:40 +02:00
Antoine Bertin 8e9781c081 Add Subtitle and Task classes. Change list and download in plugins 2011-08-29 21:20:00 +02:00
Antoine Bertin d76d81eda3 Fix release group comparison. Use lowercase 2011-08-29 20:50:34 +02:00
Antoine Bertin 7b439bbf6f Remove useless multi_filename_queries stuff 2011-08-28 09:50:01 +02:00
Antoine Bertin aea078d65f Add some features. Review module organization
- More constants (FORMATS, LANGUAGES, PLUGINS, API_PLUGINS)
- NullHandler is given to the logger
- Remove useless stuff
- Custom exceptions
- More strict setters
- Move PluginWorker class to subliminal
2011-08-27 23:44:03 +02:00
Antoine Bertin 5834c3edcf Improve argument naming in subliminal script 2011-08-27 23:29:15 +02:00
Antoine Bertin 1d40c026f4 Remove the configuration file 2011-08-27 22:55:11 +02:00
Antoine Bertin 00fb74c5b1 Replace deprecated optparse with argparse 2011-08-27 18:49:53 +02:00
Antoine Bertin 066dc77bd9 Revert to development version 2011-08-27 18:47:16 +02:00
244 changed files with 105985 additions and 2591 deletions
+8
View File
@@ -0,0 +1,8 @@
[report]
exclude_lines =
pragma: no cover
raise NotImplementedError
def __repr__
if __name__ == .__main__.:
omit =
subliminal/cli.py
+64 -4
View File
@@ -1,4 +1,64 @@
build
dist
subliminal.egg-info
*.pyc
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# 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
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Pycharm
.idea
# Subliminal
tests/data/mkv/
+49
View File
@@ -0,0 +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 -e .[test]
- if [ $PARSER = "lxml" ]; then pip install lxml; fi
- pip install coveralls
script: python setup.py test --addopts "--cov subliminal --verbose $PYTEST_ADDOPTS"
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/)
+257
View File
@@ -0,0 +1,257 @@
Changelog
---------
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
* Add login support for Addic7ed
* Remove lxml dependency
* Many fixes
0.7.0
^^^^^
**release date:** 2013-10-29
**WARNING:** Complete rewrite of subliminal with backward incompatible changes
* Use enzyme to parse metadata of videos
* Use babelfish to handle languages
* Use dogpile.cache for caching
* Use charade to detect subtitle encoding
* Use pysrt for subtitle validation
* Use entry points for subtitle providers
* New subtitle score computation
* Hearing impaired subtitles support
* Drop async support
* Drop a few providers
* And much more...
0.6.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
* Fix Addic7ed
* Fix SubsWiki
* Fix missing enzyme import
* Add Catalan and Galician languages to Addic7ed
* Add possible services in help message of the CLI
* Allow existing filenames to be passed without the ./ prefix
0.6.1
^^^^^
**release date:** 2012-06-24
* Fix subtitle release name in BierDopje
* Fix subtitles being downloaded multiple times
* Add Chinese support to TvSubtitles
* Fix encoding issues
* Fix single download subtitles without the force option
* Add Spanish (Latin America) exception to Addic7ed
* Fix group_by_video when a list entry has None as subtitles
* Add support for Galician language in Subtitulos
* Add an integrity check after subtitles download for Addic7ed
* Add error handling for if not strict in Language
* Fix TheSubDB hash method to return None if the file is too small
* Fix guessit.Language in Video.scan
* Fix language detection of subtitles
0.6.0
^^^^^
**release date:** 2012-06-16
**WARNING:** Backward incompatible changes
* Fix --workers option in CLI
* Use a dedicated module for languages
* Use beautifulsoup4
* Improve return types
* Add scan_filter option
* Add --age option in CLI
* Add TvSubtitles service
* Add Addic7ed service
0.5.1
^^^^^
**release date:** 2012-03-25
* Improve error handling of enzyme parsing
0.5
^^^
**release date:** 2012-03-25
**WARNING:** Backward incompatible changes
* Use more unicode
* New list_subtitles and download_subtitles methods
* New Pool object for asynchronous work
* Improve sort algorithm
* Better error handling
* Make sorting customizable
* Remove class Subliminal
* Remove permissions handling
0.4
^^^
**release date:** 2011-11-11
* Many fixes
* Better error handling
0.3
^^^
**release date:** 2011-08-18
* Fix a bug when series is not guessed by guessit
* Fix dependencies failure when installing package
* Fix encoding issues with logging
* Add a script to ease subtitles download
* Add possibility to choose mode of created files
* Add more checks before adjusting permissions
0.2
^^^
**release date:** 2011-07-11
* Fix plugin configuration
* Fix some encoding issues
* Remove extra logging
0.1
^^^
**release date:** *private release*
* Initial release
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
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
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+1
View File
@@ -0,0 +1 @@
include LICENSE HISTORY.rst requirements.txt
-6
View File
@@ -1,6 +0,0 @@
subliminal
==========
Python module to search and download subtitles
* [Project page](https://github.com/Diaoul/subliminal)
* [Initial project by Patrick Dessalle](http://code.google.com/p/periscope/)
+82
View File
@@ -0,0 +1,82 @@
Subliminal
==========
Subtitles, faster than your thoughts.
.. 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.svg?branch=develop
:target: https://travis-ci.org/Diaoul/subliminal
:alt: Travis CI build status
.. 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
:Project page: https://github.com/Diaoul/subliminal
:Documentation: https://subliminal.readthedocs.org/
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 / 0 error
Downloading subtitles [####################################] 100%
Downloaded 1 subtitle
Library
^^^^^^^
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 subliminal import download_best_subtitles, region, save_subtitles, scan_videos
# configure the cache
region.configure('dogpile.cache.dbm', arguments={'filename': 'cachefile.dbm'})
# scan for videos newer than 2 weeks and their existing subtitles in a folder
videos = scan_videos('/video/folder', age=timedelta(weeks=2))
# 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])
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
View File
@@ -0,0 +1 @@
-e .[dev,test]
+192
View File
@@ -0,0 +1,192 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS = -n -W
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " 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"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/subliminal.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/subliminal.qhc"
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
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/subliminal"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/subliminal"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
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
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
+21
View File
@@ -0,0 +1,21 @@
Cache
=====
.. module:: subliminal.cache
.. autodata:: SHOW_EXPIRATION_TIME
:annotation:
.. 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
+3
View File
@@ -0,0 +1,3 @@
CLI
===
.. automodule:: subliminal.cli
+7
View File
@@ -0,0 +1,7 @@
Core
====
.. automodule:: subliminal.core
:exclude-members: ARCHIVE_EXTENSIONS
.. autodata:: ARCHIVE_EXTENSIONS
:annotation:
+3
View File
@@ -0,0 +1,3 @@
Exceptions
==========
.. automodule:: subliminal.exceptions
+3
View File
@@ -0,0 +1,3 @@
Extensions
==========
.. automodule:: subliminal.extensions
+48
View File
@@ -0,0 +1,48 @@
Providers
=========
.. automodule:: subliminal.providers
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
+3
View File
@@ -0,0 +1,3 @@
Score
=====
.. automodule:: subliminal.score
+7
View File
@@ -0,0 +1,7 @@
Subtitle
========
.. automodule:: subliminal.subtitle
:exclude-members: SUBTITLE_EXTENSIONS
.. autodata:: SUBTITLE_EXTENSIONS
:annotation:
+3
View File
@@ -0,0 +1,3 @@
Utils
=====
.. automodule:: subliminal.utils
+7
View File
@@ -0,0 +1,7 @@
Video
=====
.. automodule:: subliminal.video
:exclude-members: VIDEO_EXTENSIONS
.. 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
+320
View File
@@ -0,0 +1,320 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# subliminal documentation build configuration file, created by
# sphinx-quickstart on Sat Jul 11 00:40:28 2015.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
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('..'))
import subliminal
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# 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(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.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = subliminal.__title__
copyright = ' '.join(subliminal.__copyright__.split()[1:])
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__.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.
#
# 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:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = '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 = {}
# 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 = [sphinx_rtd_theme.get_html_theme_path()]
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# 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'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# 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 = project + 'doc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# 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, or own class]).
latex_documents = [
(master_doc, project + '.tex', project + ' Documentation',
author, 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, project, project + ' Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, project, project + ' Documentation',
author, project, 'Subtitles, faster than your thoughts',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# 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')
+52
View File
@@ -0,0 +1,52 @@
.. subliminal documentation master file, created by
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.
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
-------------
.. toctree::
:maxdepth: 2
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.
.. toctree::
:maxdepth: 1
api/core
api/video
api/subtitle
api/providers
api/refiners
api/extensions
api/score
api/utils
api/cache
api/cli
api/exceptions
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
+96
View File
@@ -0,0 +1,96 @@
Provider Guide
==============
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
------------
When starting a provider you should be able to answer to the following questions:
* What languages does my provider support?
* What are the language codes for the supported languages?
* Does my provider deliver subtitles for episodes? for movies?
* Does my provider require a video hash?
Each response of these questions will help you set the correct attributes for your
:class:`~subliminal.providers.Provider`.
Video Validation
----------------
Not all providers deliver subtitles for :class:`~subliminal.video.Episode`. Some may require a hash.
The :meth:`~subliminal.providers.Provider.check` method does validation against a :class:`~subliminal.video.Video`
object and will return `False` if the given :class:`~subliminal.video.Video` isn't suitable. If you're not happy
with the default implementation, you can override it.
Configuration
-------------
API keys must not be configurable by the user and must remain linked to subliminal. Hence they must be written
in the provider module.
Per-user authentication is allowed and must be configured at instantiation as keyword arguments. Configuration
will be done by the user through the `provider_configs` argument of the :func:`~subliminal.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.ConfigurationError`.
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
----------------------------
Actual authentication operations must take place in the :meth:`~subliminal.providers.Provider.initialize` method.
If you need anything to be executed when the provider isn't used anymore like logout,
use :meth:`~subliminal.providers.Provider.terminate`.
Caching policy
--------------
To save bandwidth and improve querying time, intermediate data should be cached when possible. Typical use case is
when a query to retrieve show ids is required prior to the query to actually search for subtitles. In that case
the function that gets the show id from the show name must be cached.
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.Language`.
If you cannot find a suitable converter for your provider, you can `make one of your own
<http://babelfish.readthedocs.org/en/latest/#custom-converters>`_.
Querying
--------
The :meth:`~subliminal.providers.Provider.query` method parameters must include all aspects of provider's querying with
primary types.
Subtitle
--------
A custom :class:`~subliminal.subtitle.Subtitle` subclass must be created to represent a subtitle from the provider.
It must have relevant attributes that can be used to compute the matches of the subtitle against a
:class:`~subliminal.video.Video` object.
Score computation
-----------------
To be able to compare subtitles coming from different providers between them, the
:meth:`~subliminal.subtitle.Subtitle.get_matches` method must be implemented.
Unittesting
-----------
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
View File
@@ -0,0 +1 @@
-e .
-100
View File
@@ -1,100 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from optparse import OptionParser
import subliminal
import logging
import mimetypes
import os
import sys
def main():
'''Download subtitles'''
# parse command line options
parser = OptionParser("usage: %prog [options] file1 file2", version=subliminal.__version__)
parser.add_option("-l", "--language", action="append", dest="languages", help="wanted language (ISO 639-1 two chars) for the subtitles (e.g. fr, en). Multiple uses allowed such that `%prog -l fr -l en file1`")
parser.add_option("-m", "--multi", action="store_true", dest="multi", help="download one subtitle per specified language (instead of one of them) and name them accordingly (e.g. .fr.srt, .en.srt)")
parser.add_option("-p", "--plugin", action="append", dest="plugins", help="plugins to activate")
parser.add_option("-f", "--force", action="store_true", dest="force", help="force download of a subtitle even there is already one present")
parser.add_option("-C", "--no-config-file", action="store_false", dest="config", help="do not use configuration file (requires -l to be specified)")
parser.add_option("-c", "--config-file", action="store", dest="config", help="configuration file to use")
parser.add_option("-w", "--workers", action="store", dest="workers", help="specify the number of threads to use")
parser.add_option("--cache-dir", action="store", dest="cache_dir", help="cache directory to use")
parser.add_option("--no-cache-dir", action="store_false", dest="cache_dir", help="do not use cache directory (some plugins may not work)")
parser.add_option("--list-all-plugins", action="store_true", dest="list_all_plugins", help="list all plugins available")
parser.add_option("--list-api-plugins", action="store_true", dest="list_api_plugins", help="list api-based plugins")
parser.add_option("--list-active-plugins", action="store_true", dest="list_active_plugins", help="list currently active plugins")
parser.add_option("-v", "--verbose", action="count", dest="verbose", help="increase verbosity (maximum 2 times)")
parser.set_defaults(verbose=0, cache_dir=True, config=True, workers=4)
(options, args) = parser.parse_args()
if not args:
print parser.print_help()
exit(1)
if options.verbose == 2:
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)-24s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
elif options.verbose == 1:
logging.basicConfig(level=logging.WARN, format='%(levelname)s: %(name)s %(message)s')
else:
logging.basicConfig(level=logging.ERROR)
if not options.config and not options.languages:
parser.error("Option -C (--no-config-file) is used without -l (--language)")
subliminal_client = subliminal.Subliminal(config=options.config, cache_dir=options.cache_dir, workers=options.workers, multi=options.multi, force=options.force, max_depth=3, autostart=False)
if options.plugins:
subliminal_client.plugins = options.plugins
if options.list_all_plugins:
plugins = subliminal_client.listExistingPlugins()
print ', '.join(subliminal_client.listExistingPlugins())
exit(0)
if options.list_api_plugins:
plugins = subliminal_client.list_api_plugins()
print ', '.join(subliminal_client.listExistingPlugins())
exit(0)
if options.list_active_plugins:
plugins = subliminal_client.plugins
print ', '.join(subliminal_client.listExistingPlugins())
exit(0)
if options.languages:
subliminal_client.languages = options.languages
else:
logging.info(u"No language given, looking into configuration file")
languages = subliminal_client.languages
if not languages:
logging.error(u"No language found in configuration file")
sys.stderr.write("No language found in configuration file")
exit(1)
parser.exit
subliminal_client.startWorkers()
subtitles = subliminal_client.downloadSubtitles(args)
subliminal_client.stopWorkers()
if len(subtitles) == 0:
sys.stderr.write("No subtitles found")
exit(1)
print "*" * 50
print "Downloaded %s subtitles" % len(subtitles)
for s in subtitles:
print s['lang'] + " - " + s['subtitlepath']
print "*" * 50
if __name__ == "__main__":
main()
+10
View File
@@ -0,0 +1,10 @@
[aliases]
test=pytest
[build_sphinx]
source-dir = docs/
build-dir = docs/_build
all_files = 1
[upload_sphinx]
upload-dir = docs/_build/html
+95 -39
View File
@@ -1,45 +1,101 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import io
import os
import re
import sys
from setuptools import setup
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.5.4', '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', 'sphinxcontrib-programoutput', 'wheel']
execfile('subliminal/version.py')
setup(name='subliminal',
version=__version__,
license='LGPLv3',
description='Subliminal - Subtitles, faster than your thoughts',
classifiers=['Development Status :: 4 - Beta',
'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
'Intended Audience :: Developers',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2.6',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Multimedia :: Video'],
keywords='subliminal video movie subtitle python library',
author='Antoine Bertin',
author_email='diaoulael@gmail.com',
url='https://github.com/Diaoul/subliminal',
packages=['subliminal', 'subliminal/plugins'],
scripts=['scripts/subliminal'],
py_modules=['subliminal'],
install_requires=['BeautifulSoup>=3.2.0', 'guessit>=0.2'])
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
})
-71
View File
@@ -1,71 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import threading
import plugins
import logging
import traceback
class PluginWorker(threading.Thread):
"""Threaded plugin worker"""
def __init__(self, taskQueue, resultQueue):
threading.Thread.__init__(self)
self.taskQueue = taskQueue
self.resultQueue = resultQueue
self.logger = logging.getLogger('subliminal.worker')
def run(self):
while True:
task = self.taskQueue.get()
result = None
try:
if not task: # this is a poison pill
break
elif task['task'] == 'list': # the task is a listing
# get the corresponding plugin
plugin = getattr(plugins, task['plugin'])(task['config'])
# split tasks if the plugin can't handle multi queries
splitedTasks = plugin.splitTask(task)
myTask = splitedTasks.pop()
for st in splitedTasks:
self.taskQueue.put(st)
result = plugin.list(myTask['filenames'], myTask['languages'])
elif task['task'] == 'download': # the task is to download
result = None
while task['subtitle']:
subtitle = task['subtitle'].pop(0)
# get the corresponding plugin
plugin = getattr(plugins, subtitle['plugin'])(task['config'])
path = plugin.download(subtitle)
if path:
subtitle['subtitlepath'] = path
result = subtitle
break
else:
self.logger.error(u'Unknown task %s submited to worker %s' % (task['task'], self.name))
except:
self.logger.debug(traceback.print_exc())
self.logger.error(u"Worker couldn't do the job %s, continue anyway" % task['task'])
finally:
self.resultQueue.put(result)
self.taskQueue.task_done()
self.logger.debug(u'Thread %s terminated' % self.name)
+19 -21
View File
@@ -1,23 +1,21 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__title__ = 'subliminal'
__version__ = '2.0.1'
__short_version__ = '.'.join(__version__.split('.')[:2])
__author__ = 'Antoine Bertin'
__license__ = 'MIT'
__copyright__ = 'Copyright 2016, Antoine Bertin'
from subliminal import Subliminal
from version import __version__
import logging
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())
+16
View File
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
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()
region = make_region()
+459
View File
@@ -0,0 +1,459 @@
# -*- coding: utf-8 -*-
"""
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
from appdirs import AppDirs
from babelfish import Error as BabelfishError, Language
import click
from dogpile.cache.backends.file import AbstractFileLock
from dogpile.core 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__)
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()
class Config(object):
"""A :class:`~configparser.ConfigParser` wrapper to store configuration.
Interaction with the configuration is done with the properties.
:param str path: path to the configuration file.
"""
def __init__(self, path):
#: Path to the configuration file
self.path = path
#: 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))
def read(self):
"""Read the configuration from :attr:`path`"""
self.config.read(self.path)
def write(self):
"""Write the configuration to :attr:`path`"""
with open(self.path, 'w') as f:
self.config.write(f)
@property
def languages(self):
return {Language.fromietf(l) for l in json.loads(self.config.get('general', 'languages'))}
@languages.setter
def languages(self, value):
self.config.set('general', 'languages', json.dumps(sorted([str(l) for l in value])))
@property
def providers(self):
return json.loads(self.config.get('general', 'providers'))
@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:
self.fail('%s is not a valid age' % value)
return timedelta(**{k: int(v) for k, v in match.groupdict(0).items()})
AGE = AgeParamType()
PROVIDER = click.Choice(sorted(provider_manager.names()))
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
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 = []
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)
# 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
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
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 ''))
View File
+32
View File
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from babelfish import LanguageReverseConverter, language_converters
class Addic7edConverter(LanguageReverseConverter):
def __init__(self):
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, script=None):
if (alpha3, country, script) in self.to_addic7ed:
return self.to_addic7ed[(alpha3, country, script)]
if (alpha3, country) in self.to_addic7ed:
return self.to_addic7ed[(alpha3, country)]
if (alpha3,) in self.to_addic7ed:
return self.to_addic7ed[(alpha3,)]
return self.name_converter.convert(alpha3, country, script)
def reverse(self, addic7ed):
if addic7ed in self.from_addic7ed:
return self.from_addic7ed[addic7ed]
return self.name_converter.reverse(addic7ed)
+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)
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from babelfish import LanguageReverseConverter, language_converters
class TVsubtitlesConverter(LanguageReverseConverter):
def __init__(self):
self.alpha2_converter = language_converters['alpha2']
self.from_tvsubtitles = {'br': ('por', 'BR'), 'ua': ('ukr',), 'gr': ('ell',), 'cn': ('zho',), 'jp': ('jpn',),
'cz': ('ces',)}
self.to_tvsubtitles = {v: k for k, v in self.from_tvsubtitles.items()}
self.codes = self.alpha2_converter.codes | set(self.from_tvsubtitles.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3, country) in self.to_tvsubtitles:
return self.to_tvsubtitles[(alpha3, country)]
if (alpha3,) in self.to_tvsubtitles:
return self.to_tvsubtitles[(alpha3,)]
return self.alpha2_converter.convert(alpha3, country, script)
def reverse(self, tvsubtitles):
if tvsubtitles in self.from_tvsubtitles:
return self.from_tvsubtitles[tvsubtitles]
return self.alpha2_converter.reverse(tvsubtitles)
+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
-66
View File
@@ -1,66 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 Nic Wolfe <nic@wolfeden.ca>
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import os
import subliminal
# This module tries to deal with the apparently random behavior of python when dealing with unicode <-> utf-8
# encodings. It tries to just use unicode, but if that fails then it tries forcing it to utf-8. Any functions
# which return something should always return unicode.
def fixStupidEncodings(x, silent=False):
if type(x) == str:
try:
return x.decode(subliminal.SYS_ENCODING)
except UnicodeDecodeError:
subliminal.logger.error(u"Unable to decode value: " + repr(x))
return None
elif type(x) == unicode:
return x
else:
subliminal.logger.log(u"Unknown value passed in, ignoring it: " + str(type(x)) + " (" + repr(x) + ":" + repr(type(x)) + ")", logging.DEBUG if silent else logging.ERROR)
return None
return None
def fixListEncodings(x):
if type(x) != list:
return x
else:
return filter(lambda x: x != None, map(fixStupidEncodings, x))
def ek(func, *args):
result = None
if os.name == 'nt':
result = func(*args)
else:
result = func(*[x.encode(subliminal.SYS_ENCODING) if type(x) in (str, unicode) else x for x in args])
if type(result) == list:
return fixListEncodings(result)
elif type(result) == str:
return fixStupidEncodings(result)
else:
return result
+29
View File
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
class Error(Exception):
"""Base class for exceptions in subliminal."""
pass
class ProviderError(Error):
"""Exception raised by providers."""
pass
class ConfigurationError(ProviderError):
"""Exception raised by providers when badly configured."""
pass
class AuthenticationError(ProviderError):
"""Exception raised by providers when authentication failed."""
pass
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'
])
-158
View File
@@ -1,158 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from BeautifulSoup import BeautifulSoup
import guessit
import PluginBase
import zipfile
import os
import urllib2
import urllib
import traceback
import httplib
import re
import socket
class Addic7ed(PluginBase.PluginBase):
site_url = 'http://www.addic7ed.com'
site_name = 'Addic7ed'
server_url = 'http://www.addic7ed.com'
multi_languages_queries = True
multi_filename_queries = False
api_based = False
_plugin_languages = {u"English": "en",
u"English (US)": "en",
u"English (UK)": "en",
u"Italian": "it",
u"Portuguese": "pt",
u"Portuguese (Brazilian)": "pt-br",
u"Romanian": "ro",
u"Español (Latinoamérica)": "es",
u"Español (España)": "es",
u"Spanish (Latin America)": "es",
u"Español": "es",
u"Spanish": "es",
u"Spanish (Spain)": "es",
u"French": "fr",
u"Greek": "el",
u"Arabic": "ar",
u"German": "de",
u"Croatian": "hr",
u"Indonesian": "id",
u"Hebrew": "he",
u"Russian": "ru",
u"Turkish": "tr",
u"Swedish": "se",
u"Czech": "cs",
u"Dutch": "nl",
u"Hungarian": "hu",
u"Norwegian": "no",
u"Polish": "pl",
u"Persian": "fa"}
def __init__(self, config_dict=None):
super(Addic7ed, self).__init__(self._plugin_languages, config_dict, isRevert=True)
#http://www.addic7ed.com/serie/Smallville/9/11/Absolute_Justice
self.release_pattern = re.compile(" \nVersion (.+), ([0-9]+).([0-9])+ MBs")
def list(self, filenames, languages):
''' Main method to call when you want to list subtitles '''
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
if not self.checkLanguages(languages):
return []
filepath = filenames[0]
guess = guessit.guess_file_info(filepath, 'autodetect')
if guess['type'] != 'episode':
return []
# add multiple things to the release group set
release_group = set()
if 'releaseGroup' in guess:
release_group.add(guess['releaseGroup'])
else:
if 'title' in guess:
release_group.add(guess['title'])
if 'screenSize' in guess:
release_group.add(guess['screenSize'])
if 'series' not in guess or len(release_group) == 0:
return []
self.release_group = release_group # used to sort results
return self.query(guess['series'], guess['season'], guess['episodeNumber'], release_group, filepath, languages)
def query(self, name, season, episode, release_group, filepath, languages=None):
''' Make a query and returns info about found subtitles '''
searchname = name.lower().replace(" ", "_")
searchurl = "%s/serie/%s/%s/%s/%s" % (self.server_url, searchname, season, episode, searchname)
self.logger.debug(u"Searching in %s" % searchurl)
try:
req = urllib2.Request(searchurl, headers={'User-Agent': self.user_agent})
page = urllib2.urlopen(req, timeout=self.timeout)
except urllib2.HTTPError as inst:
self.logger.info(u"Error: %s - %s" % (searchurl, inst))
return []
except urllib2.URLError as inst:
self.logger.info(u"TimeOut: %s" % inst)
return []
soup = BeautifulSoup(page.read())
sublinks = []
for html_sub in soup("td", {"class": "NewsTitle", "colspan": "3"}):
if not self.release_pattern.match(str(html_sub.contents[1])): # On not needed soup td result
continue
sub_teams = self.listTeams([self.release_pattern.match(str(html_sub.contents[1])).groups()[0]], [".", "_", " "])
if not release_group.intersection(sub_teams): # On wrong team
continue
html_language = html_sub.findNext("td", {"class": "language"})
sub_language = self.getRevertLanguage(html_language.contents[0].strip().replace('&nbsp;', ''))
if languages and not sub_language in languages: # On wrong language
continue
html_status = html_language.findNextSibling('td')
sub_status = html_status.find('b').string.strip()
if not sub_status == 'Completed': # On not completed subtitles
continue
sub_link = self.server_url + html_status.findNextSibling('td', {'colspan': '3'}).find('a')['href']
self.logger.debug(u'Found a match with teams: %s' % sub_teams)
result = {}
result["release"] = "%s.S%.2dE%.2d.%s" % (name.replace(" ", "."), int(season), int(episode), '.'.join(sub_teams))
result["lang"] = sub_language
result["link"] = sub_link
result["page"] = searchurl
result["filename"] = filepath
result["plugin"] = self.getClassName()
result["teams"] = sub_teams # used to sort
sublinks.append(result)
sublinks.sort(self._cmpTeams)
return sublinks
def download(self, subtitle):
'''pass the URL of the sub and the file it matches, will unzip it
and return the path to the created file'''
suburl = subtitle["link"]
videofilename = subtitle["filename"]
srtbasefilename = videofilename.rsplit(".", 1)[0]
srtfilename = srtbasefilename + self.getExtension(subtitle)
self.downloadFile(suburl, srtfilename)
return srtfilename
def _cmpTeams(self, x, y):
''' Sort based on teams matching '''
return -cmp(len(x['teams'].intersection(self.release_group)), len(y['teams'].intersection(self.release_group)))
-170
View File
@@ -1,170 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from xml.dom import minidom
import guessit
import PluginBase
import os
import pickle
import traceback
import urllib
import urllib2
from subliminal import encodingKludge as ek
class BierDopje(PluginBase.PluginBase):
site_url = 'http://bierdopje.com'
site_name = 'BierDopje'
server_url = 'http://api.bierdopje.com/A2B638AC5D804C2E/'
multi_languages_queries = True
multi_filename_queries = False
api_based = True
exceptions = {'the office': 10358,
'the office us': 10358,
'greys anatomy': 3733,
'sanctuary us': 7904,
'human target 2010': 12986,
'csi miami': 2187,
'castle 2009': 12708,
'chase 2010': 14228,
'the defenders 2010': 14225,
'hawaii five-0 2010': 14211}
_plugin_languages = {'en': 'en', 'nl': 'nl'}
def __init__(self, config_dict):
super(BierDopje, self).__init__(self._plugin_languages, config_dict)
#http://api.bierdopje.com/23459DC262C0A742/GetShowByName/30+Rock
#http://api.bierdopje.com/23459DC262C0A742/GetAllSubsFor/94/5/1/en (30 rock, season 5, episode 1)
if not config_dict or not config_dict['cache_dir']:
raise Exception('Cache directory is mandatory for this plugin')
self.showid_cache = ek.ek(os.path.join, config_dict['cache_dir'], "bierdopje_showid.cache")
with self.lock:
if not ek.ek(os.path.exists, self.showid_cache):
if not ek.ek(os.path.exists, ek.ek(os.path.dirname, self.showid_cache)):
raise Exception("Cache directory doesn't exists")
f = open(self.showid_cache, 'w')
pickle.dump({}, f)
f.close()
f = open(self.showid_cache, 'r')
self.showids = pickle.load(f)
self.logger.debug(u"Reading showids from cache: %s" % self.showids)
f.close()
def list(self, filenames, languages):
"""Main method to call when you want to list subtitles"""
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
if not self.checkLanguages(languages):
return []
filepath = filenames[0]
guess = guessit.guess_file_info(filepath, 'autodetect')
if guess['type'] != 'episode':
return []
# add multiple things to the release group set
release_group = set()
if 'releaseGroup' in guess:
release_group.add(guess['releaseGroup'].lower())
else:
if 'title' in guess:
release_group.add(guess['title'].lower())
if 'screenSize' in guess:
release_group.add(guess['screenSize'].lower())
if 'series' not in guess or len(release_group) == 0:
return []
self.release_group = release_group # used to sort results
return self.query(guess['series'], guess['season'], guess['episodeNumber'], release_group, filepath, languages)
def download(self, subtitle):
"""Main method to call when you want to download a subtitle"""
subpath = subtitle["filename"].rsplit(".", 1)[0] + self.getExtension(subtitle)
self.downloadFile(subtitle["link"], subpath)
return subpath
def query(self, name, season, episode, release_group, filepath, languages=None):
"""Makes a query and returns info (link, lang) about found subtitles"""
if languages:
available_languages = list(set(languages).intersection((self._plugin_languages.values())))
else:
available_languages = self._plugin_languages.values()
sublinks = []
# get the show id
show_name = name.lower()
if show_name in self.exceptions: # get it from exceptions
show_id = self.exceptions[show_name]
elif show_name in self.showids: # get it from cache
show_id = self.showids[show_name]
else: # retrieve it
show_id_url = "%sGetShowByName/%s" % (self.server_url, urllib.quote(show_name))
self.logger.debug(u"Retrieving show id from web at %s" % show_id_url)
page = urllib2.urlopen(show_id_url)
dom = minidom.parse(page)
if not dom or len(dom.getElementsByTagName('showid')) == 0: # no proper result
page.close()
return []
show_id = dom.getElementsByTagName('showid')[0].firstChild.data
self.showids[show_name] = show_id
with self.lock:
f = open(self.showid_cache, 'w')
self.logger.debug(u"Writing showid %s to cache file" % show_id)
pickle.dump(self.showids, f)
f.close()
page.close()
# get the subs for the show id we have
for language in available_languages:
subs_url = "%sGetAllSubsFor/%s/%s/%s/%s" % (self.server_url, show_id, season, episode, language)
self.logger.debug(u"Getting subtitles at %s" % subs_url)
page = urllib2.urlopen(subs_url)
dom = minidom.parse(page)
page.close()
for sub in dom.getElementsByTagName('result'):
sub_release = sub.getElementsByTagName('filename')[0].firstChild.data
if sub_release.endswith(".srt"):
sub_release = sub_release[:-4]
sub_release = sub_release + '.avi' # put a random extension for guessit not to fail guessing that file
# guess information from subtitle
sub_guess = guessit.guess_file_info(sub_release, 'episode')
sub_release_group = set()
if 'releaseGroup' in sub_guess:
sub_release_group.add(sub_guess['releaseGroup'].lower())
else:
if 'title' in sub_guess:
sub_release_group.add(sub_guess['title'].lower())
if 'screenSize' in sub_guess:
sub_release_group.add(sub_guess['screenSize'].lower())
sub_link = sub.getElementsByTagName('downloadlink')[0].firstChild.data
result = {}
result["release"] = sub_release
result["link"] = sub_link
result["page"] = sub_link
result["lang"] = language
result["filename"] = filepath
result["plugin"] = self.getClassName()
result["releaseGroup"] = sub_release_group
sublinks.append(result)
sublinks.sort(self._cmpReleaseGroup)
return sublinks
def _cmpReleaseGroup(self, x, y):
"""Sort based on teams matching"""
return -cmp(len(x['releaseGroup'].intersection(self.release_group)), len(y['releaseGroup'].intersection(self.release_group)))
-206
View File
@@ -1,206 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import PluginBase
import gzip
import os
import socket
import xmlrpclib
import guessit
from subliminal import encodingKludge as ek
class OpenSubtitles(PluginBase.PluginBase):
site_url = 'http://www.opensubtitles.org'
site_name = 'OpenSubtitles'
server_url = 'http://api.opensubtitles.org/xml-rpc'
user_agent = 'Subliminal v0.3'
multi_languages_queries = True
multi_filename_queries = False
api_based = True
_plugin_languages = {"en": "eng",
"fr": "fre",
"hu": "hun",
"cs": "cze",
"pl": "pol",
"sk": "slo",
"pt": "por",
"pt-br": "pob",
"es": "spa",
"el": "ell",
"ar": "ara",
"sq": "alb",
"hy": "arm",
"ay": "ass",
"bs": "bos",
"bg": "bul",
"ca": "cat",
"zh": "chi",
"hr": "hrv",
"da": "dan",
"nl": "dut",
"eo": "epo",
"et": "est",
"fi": "fin",
"gl": "glg",
"ka": "geo",
"de": "ger",
"he": "heb",
"hi": "hin",
"is": "ice",
"id": "ind",
"it": "ita",
"ja": "jpn",
"kk": "kaz",
"ko": "kor",
"lv": "lav",
"lt": "lit",
"lb": "ltz",
"mk": "mac",
"ms": "may",
"no": "nor",
"oc": "oci",
"fa": "per",
"ro": "rum",
"ru": "rus",
"sr": "scc",
"sl": "slv",
"sv": "swe",
"th": "tha",
"tr": "tur",
"uk": "ukr",
"vi": "vie"}
def __init__(self, config_dict=None):
super(OpenSubtitles, self).__init__(self._plugin_languages, config_dict)
def list(self, filenames, languages):
"""Main method to call when you want to list subtitles """
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
filepath = filenames[0]
if ek.ek(os.path.isfile, filepath):
filehash = self.hashFile(filepath)
size = ek.ek(os.path.getsize, filepath)
return self.query(moviehash=filehash, languages=languages, bytesize=size, filepath=filepath)
else:
return self.query(languages=languages, filepath=filepath)
def download(self, subtitle):
"""Main method to call when you want to download a subtitle """
subtitleFilename = subtitle["filename"].rsplit(".", 1)[0] + self.getExtension(subtitle)
self.downloadFile(subtitle["link"], subtitleFilename + ".gz")
f = ek.ek(gzip.open, subtitleFilename + ".gz")
dump = ek.ek(open, subtitleFilename, "wb")
dump.write(f.read())
self.adjustPermissions(subtitleFilename)
dump.close()
f.close()
ek.ek(os.remove, subtitleFilename + ".gz")
return subtitleFilename
def query(self, filepath, imdbID=None, moviehash=None, bytesize=None, languages=None):
"""Makes a query on OpenSubtitles and returns info about found subtitles.
Note: if using moviehash, bytesize is required. """
# prepare the search
search = {}
if moviehash:
search['moviehash'] = moviehash
if imdbID:
search['imdbid'] = imdbID
if bytesize:
search['moviebytesize'] = str(bytesize)
if languages:
search['sublanguageid'] = ",".join([self.getLanguage(l) for l in languages])
if not imdbID and not moviehash and not bytesize:
self.logger.debug(u"No search term, we'll use the filename")
guess = guessit.guess_file_info(filepath, 'autodetect')
if guess['type'] == 'episode' and 'series' in guess:
search['query'] = guess['series']
elif guess['type'] == 'movie':
search['query'] = guess['title']
else: # we don't know what we have
return[]
# login
self.server = xmlrpclib.Server(self.server_url)
socket.setdefaulttimeout(self.timeout)
try:
log_result = self.server.LogIn("", "", "eng", self.user_agent)
if not log_result["status"] or log_result["status"] != '200 OK' or not log_result["token"]:
raise Exception('OpenSubtitles login failed')
token = log_result["token"]
except Exception:
self.logger.error(u"Cannot login")
token = None
socket.setdefaulttimeout(None)
return []
# search
sublinks = self.get_results(token, search, filepath)
# logout
try:
self.server.LogOut(token)
except:
self.logger.error(u"Cannot logout")
socket.setdefaulttimeout(None)
return sublinks
def get_results(self, token, search, filepath):
self.logger.debug(u"Query uses token %s and search parameters %s" % (token, search))
try:
results = self.server.SearchSubtitles(token, [search])
except Exception, e:
self.logger.debug(u"Cannot query the server")
return []
if not results['data']: # no subtitle found
return []
sublinks = []
self.filename = self.getFileName(filepath)
for r in sorted(results['data'], self._cmpSubFileName):
result = {}
result["release"] = r['SubFileName']
result["link"] = r['SubDownloadLink']
result["page"] = r['SubDownloadLink']
result["lang"] = self.getRevertLanguage(r['SubLanguageID'])
result["filename"] = filepath
result["plugin"] = self.getClassName()
if 'query' in search and not r["MovieReleaseName"].replace('.', ' ').startswith(search['query']): # query mode search, filter results
self.logger.debug(u"Skipping %s it does not start with %s" % (r["MovieReleaseName"].replace('.', ' '), search['query']))
continue
sublinks.append(result)
return sublinks
def _cmpSubFileName(self, x, y):
"""Sort based on the SubFileName name tag """
#TODO add also support for subtitles release
xmatch = x['SubFileName'] and (x['SubFileName'].find(self.filename) > -1 or self.filename.find(x['SubFileName']) > -1)
ymatch = y['SubFileName'] and (y['SubFileName'].find(self.filename) > -1 or self.filename.find(y['SubFileName']) > -1)
if xmatch and ymatch:
if x['SubFileName'] == self.filename or x['SubFileName'].startswith(self.filename):
return - 1
return 0
if not xmatch and not ymatch:
return 0
if xmatch and not ymatch:
return - 1
if not xmatch and ymatch:
return 1
return 0
-188
View File
@@ -1,188 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import abc
import logging
import os
import re
import sys
import urllib2
import struct
import threading
from subliminal import encodingKludge as ek
class PluginBase(object):
__metaclass__ = abc.ABCMeta
multi_languages_queries = False
multi_filename_queries = False
api_based = True
timeout = 3
user_agent = 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.3)'
lock = threading.Lock()
@abc.abstractmethod
def __init__(self, pluginLanguages, config_dict=None, isRevert=False):
self.config_dict = config_dict
if not pluginLanguages:
self.pluginLanguages = None
self.revertPluginLanguages = None
elif not isRevert:
self.pluginLanguages = pluginLanguages
self.revertPluginLanguages = dict((v, k) for k, v in self.pluginLanguages.iteritems())
else:
self.revertPluginLanguages = pluginLanguages
self.pluginLanguages = dict((v, k) for k, v in self.revertPluginLanguages.iteritems())
self.logger = logging.getLogger('subliminal.%s' % self.getClassName())
@staticmethod
def getFileName(filepath):
filename = filepath
if ek.ek(os.path.isfile, filename):
filename = ek.ek(os.path.basename, filename)
if filename.endswith(('.avi', '.wmv', '.mov', '.mp4', '.mpeg', '.mpg', '.mkv')):
filename = filename.rsplit('.', 1)[0]
return filename
def hashFile(self, filename):
"""Hash a file like OpenSubtitles"""
longlongformat = 'q' # long long
bytesize = struct.calcsize(longlongformat)
f = ek.ek(open, filename, "rb")
filesize = ek.ek(os.path.getsize, filename)
hash = filesize
if filesize < 65536 * 2:
self.logger.error(u"File %s is too small (SizeError < 2**16)" % filename)
return []
for x in range(65536 / bytesize):
buffer = f.read(bytesize)
(l_value,) = struct.unpack(longlongformat, buffer)
hash += l_value
hash = hash & 0xFFFFFFFFFFFFFFFF # to remain as 64bit number
f.seek(max(0, filesize - 65536), 0)
for x in range(65536 / bytesize):
buffer = f.read(bytesize)
(l_value,) = struct.unpack(longlongformat, buffer)
hash += l_value
hash = hash & 0xFFFFFFFFFFFFFFFF
f.close()
returnedhash = "%016x" % hash
return returnedhash
def downloadFile(self, url, filename, data=None):
"""Downloads the given url to the given filename"""
try:
self.logger.info(u"Downloading %s" % url)
req = urllib2.Request(url, headers={'Referer': url, 'User-Agent': self.user_agent})
f = urllib2.urlopen(req, data=data)
dump = ek.ek(open, filename, "wb")
dump.write(f.read())
self.adjustPermissions(filename)
dump.close()
f.close()
self.logger.debug(u"Download finished for file %s. Size: %s" % (filename, ek.ek(os.path.getsize, filename)))
except urllib2.HTTPError, e:
self.logger.error(u"HTTP Error:", e.code, url)
except urllib2.URLError, e:
self.logger.error(u"URL Error:", e.reason, url)
def adjustPermissions(self, filepath):
if self.config_dict and 'files_mode' in self.config_dict and self.config_dict['files_mode'] != -1:
ek.ek(os.chmod, filepath, self.config_dict['files_mode'])
@abc.abstractmethod
def list(self, filenames, languages):
"""Main method to call when you want to list subtitles"""
@abc.abstractmethod
def download(self, subtitle):
"""Main method to call when you want to download a subtitle"""
def getRevertLanguage(self, language):
"""Returns the short (two-character) representation from the long language name"""
try:
return self.revertPluginLanguages[language]
except KeyError, e:
self.logger.warn(u"Ooops, you found a missing language in the configuration file of %s: %s. Send a bug report to have it added." % (self.getClassName(), language))
def checkLanguages(self, languages):
if languages and not set(languages).intersection((self._plugin_languages.values())):
self.logger.debug(u'None of requested languages %s are available' % languages)
return False
return True
def getLanguage(self, language):
"""Returns the long naming of the language from a two character code"""
try:
return self.pluginLanguages[language]
except KeyError, e:
self.logger.warn(u"Ooops, you found a missing language in the configuration file of %s: %s. Send a bug report to have it added." % (self.getClassName(), language))
def getExtension(self, subtitle):
if self.config_dict and self.config_dict['multi']:
return ".%s.srt" % subtitle['lang']
return ".srt"
def getClassName(self):
return self.__class__.__name__
def splitTask(self, task):
"""Determines if the plugin can handle multi-thing queries and output splited tasks for list task only"""
if task['task'] != 'list':
return [task]
tasks = [task]
if not self.multi_filename_queries:
tasks = self._splitOnField(tasks, 'filenames')
if not self.multi_languages_queries:
tasks = self._splitOnField(tasks, 'languages')
return tasks
@staticmethod
def _splitOnField(elements, field):
"""
Split a list of dict in a bigger one if the element field in the dict has multiple elements too
i.e. [{'a': 1, 'b': [2,3]}, {'a': 7, 'b': [4]}] => [{'a': 1, 'b': [2]}, {'a': 1, 'b': [3]}, {'a': 7, 'b': [4]}]
with field = 'b'
"""
results = []
for e in elements:
for v in e[field]:
newElement = {}
for (key, value) in e.items():
if key != field:
newElement[key] = value
else:
newElement[key] = [v]
results.append(newElement)
return results
def listTeams(self, sub_teams, separators):
"""List teams of a given string using separators"""
for sep in separators:
sub_teams = self.splitTeam(sub_teams, sep)
return set(sub_teams)
def splitTeam(self, sub_teams, sep):
"""Split teams of a given string using separators"""
teams = []
for t in sub_teams:
teams += t.split(sep)
return teams
-146
View File
@@ -1,146 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from hashlib import md5, sha256
import PluginBase
import xmlrpclib
import struct
import socket
import zipfile
import os
import urllib2
import urllib
import traceback
from subliminal import encodingKludge as ek
class Podnapisi(PluginBase.PluginBase):
site_url = "http://www.podnapisi.net"
site_name = "Podnapisi"
server_url = 'http://ssp.podnapisi.net:8000'
multi_languages_queries = True
multi_filename_queries = False
api_based = True
_plugin_languages = {"sl": "1",
"en": "2",
"no": "3",
"ko": "4",
"de": "5",
"is": "6",
"cs": "7",
"fr": "8",
"it": "9",
"bs": "10",
"ja": "11",
"ar": "12",
"ro": "13",
"es-ar": "14",
"hu": "15",
"el": "16",
"zh": "17",
"lt": "19",
"et": "20",
"lv": "21",
"he": "22",
"nl": "23",
"da": "24",
"se": "25",
"pl": "26",
"ru": "27",
"es": "28",
"sq": "29",
"tr": "30",
"fi": "31",
"pt": "32",
"bg": "33",
"mk": "35",
"sk": "37",
"hr": "38",
"zh": "40",
"hi": "42",
"th": "44",
"uk": "46",
"sr": "47",
"pt-br": "48",
"ga": "49",
"be": "50",
"vi": "51",
"fa": "52",
"ca": "53",
"id": "54"}
def __init__(self, config_dict=None):
super(Podnapisi, self).__init__(self._plugin_languages, config_dict)
# Podnapisi uses two reference for latin serbian and cyrillic serbian (36 and 47)
# add the 36 manually as cyrillic seems to be more used
self.revertPluginLanguages["36"] = "sr"
def list(self, filenames, languages):
"""Main method to call when you want to list subtitles"""
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
filepath = filenames[0]
if not ek.ek(os.path.isfile, filepath):
return []
return self.query(self.hashFile(filepath), languages)
def download(self, subtitle):
return []
def query(self, moviehash, languages=None):
"""Makes a query on podnapisi and returns info (link, lang) about found subtitles"""
# login
self.server = xmlrpclib.Server(self.server_url)
socket.setdefaulttimeout(self.timeout)
try:
log_result = self.server.initiate(self.user_agent)
self.logger.debug(u"Result: %s" % log_result)
token = log_result["session"]
nonce = log_result["nonce"]
except Exception, e:
self.logger.error(u"Cannot login" % log_result)
socket.setdefaulttimeout(None)
return []
username = 'getmesubs'
password = '99D31$$'
hash = md5()
hash.update(password)
password = hash.hexdigest()
hash = sha256()
hash.update(password)
hash.update(nonce)
password = hash.hexdigest()
self.server.authenticate(token, username, password)
self.logger.debug(u'Authenticated')
#if languages:
# self.logger.debug([self.getLanguage(l) for l in languages])
# self.server.setFilters(token, [self.getLanguage(l) for l in languages])
# self.logger.debug('Filers set for languages %s' % languages)
self.logger.debug(u"Starting search with token %s and hashs %s" % (token, [moviehash]))
results = self.server.search(token, [moviehash])
return results
subs = []
for sub in results['results']:
subs.append(sub)
d = self.server.download(token, [173793])
self.server.terminate(token)
return subs
-177
View File
@@ -1,177 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from BeautifulSoup import BeautifulSoup
import PluginBase
import zipfile
import os
import urllib2
import urllib
import traceback
import httplib
from subliminal import encodingKludge as ek
class SubScene(PluginBase.PluginBase):
site_url = 'http://subscene.com'
site_name = 'SubScene'
server_url = 'http://subscene.com/s.aspx?subtitle='
multi_languages_queries = True
multi_filename_queries = False
api_based = False
_plugin_languages = {"en": "English",
"se": "Swedish",
"da": "Danish",
"fi": "Finnish",
"no": "Norwegian",
"fr": "French",
"es": "Spanish",
"is": "Icelandic",
"cs": "Czech",
"bg": "Bulgarian",
"de": "German",
"ar": "Arabic",
"el": "Greek",
"fa": "Farsi/Persian",
"nl": "Dutch",
"he": "Hebrew",
"id": "Indonesian",
"ja": "Japanese",
"vi": "Vietnamese",
"pt": "Portuguese",
"ro": "Romanian",
"tr": "Turkish",
"sr": "Serbian",
"pt-br": "Brazillian Portuguese",
"ru": "Russian",
"hr": "Croatian",
"sl": "Slovenian",
"zh": "Chinese BG code",
"it": "Italian",
"pl": "Polish",
"ko": "Korean",
"hu": "Hungarian",
"ku": "Kurdish",
"et": "Estonian"}
def __init__(self, config_dict=None):
super(SubScene, self).__init__(self._plugin_languages, config_dict)
#http://subscene.com/s.aspx?subtitle=Dexter.S04E01.HDTV.XviD-NoTV
def list(self, filenames, languages):
"""Main method to call when you want to list subtitles"""
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
filepath = filenames[0]
fname = self.getFileName(filepath)
subs = self.query(fname, filepath, languages)
if not subs and fname.rfind(".[") > 0:
# Try to remove the [VTV] or [EZTV] at the end of the file
teamless_filename = fname[0:fname.rfind(".[")]
subs = self.query(teamless_filename, filepath, languages)
return subs
else:
return subs
def download(self, subtitle):
"""Main method to call when you want to download a subtitle"""
subpage = subtitle["page"]
page = urllib2.urlopen(subpage)
soup = BeautifulSoup(page)
dlhref = soup.find("div", {"class": "download"}).find("a")["href"]
subtitle["link"] = self.site_url + dlhref.split('"')[7]
format = "zip"
archivefilename = subtitle["filename"].rsplit(".", 1)[0] + '.' + format
self.downloadFile(subtitle["link"], archivefilename)
subtitlefilename = None
if zipfile.is_zipfile(archivefilename):
self.logger.debug(u"Unzipping file " + archivefilename)
zf = zipfile.ZipFile(archivefilename, "r")
for el in zf.infolist():
extension = el.orig_filename.rsplit(".", 1)[1]
if extension in ("srt", "sub", "txt"):
subtitlefilename = srtbasefilename + "." + extension
outfile = ek.ek(open, subtitlefilename, "wb")
outfile.write(zf.read(el.orig_filename))
outfile.flush()
self.adjustPermissions(subtitlefilename)
outfile.close()
else:
self.logger.info(u"File %s does not seem to be valid " % el.orig_filename)
# Deleting the zip file
zf.close()
ek.ek(os.remove, archivefilename)
return subtitlefilename
elif archivefilename.endswith('.rar'):
self.logger.warn(u'Rar is not really supported yet. Trying to call unrar')
import subprocess
try:
args = ['unrar', 'lb', archivefilename]
output = subprocess.Popen(args, stdout=subprocess.PIPE).communicate()[0]
for el in output.splitlines():
extension = el.rsplit(".", 1)[1]
if extension in ("srt", "sub"):
args = ['unrar', 'e', archivefilename, el, ek.ek(os.path.dirname, archivefilename)]
subprocess.Popen(args)
tmpsubtitlefilename = ek.ek(os.path.join, ek.ek(os.path.dirname, archivefilename), el)
subtitlefilename = ek.ek(os.path.join, ek.ek(os.path.dirname, archivefilename), srtbasefilename + "." + extension)
if ek.ek(os.path.exists, tmpsubtitlefilename):
# rename it to match the file
ek.ek(os.rename, tmpsubtitlefilename, subtitlefilename)
# exit
return subtitlefilename
except OSError, e:
self.logger.error(u"Execution failed: %s" % e)
return None
else:
self.logger.info(u"Unexpected file type (not zip) for %s" % archivefilename)
return None
def downloadFile(self, url, filename):
"""Downloads the given url to the given filename"""
#FIXME: Not working
super(SubScene, self).downloadFile(url, filename, urllib.urlencode({'__EVENTTARGET': 's$lc$bcr$downloadLink', '__EVENTARGUMENT': '', '__VIEWSTATE': '/wEPDwUHNzUxOTkwNWRk4wau5efPqhlBJJlOkKKHN8FIS04='}))
def query(self, token, filepath, langs=None):
"""Make a query on SubScene and returns info about found subtitles"""
sublinks = []
searchurl = "%s%s" % (self.server_url, urllib.quote(token))
self.logger.debug(u"Query: %s" % searchurl)
page = urllib2.urlopen(searchurl)
soup = BeautifulSoup(page.read())
for subs in soup("a", {"class": "a1"}):
lang_span = subs.find("span")
lang = self.getRevertLanguage(lang_span.contents[0].strip())
release_span = lang_span.findNext("span")
release = release_span.contents[0].strip().split(" (")[0]
sub_page = subs["href"]
#http://subscene.com//s-dlpath-260016/78348/rar.zipx
if release.lower().startswith(token.lower()) and (not langs or lang in langs):
result = {}
result["release"] = release
result["lang"] = lang
result["link"] = None
result["page"] = self.site_url + sub_page
result["filename"] = filepath
result["plugin"] = self.getClassName()
sublinks.append(result)
return sublinks
-156
View File
@@ -1,156 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from BeautifulSoup import BeautifulSoup
import PluginBase
import zipfile
import urllib2
import urllib
import logging
import traceback
import httplib
import re
import guessit
from subliminal import encodingKludge as ek
class SubsWiki(PluginBase.PluginBase):
site_url = 'http://www.subswiki.com'
site_name = 'SubsWiki'
server_url = 'http://www.subswiki.com'
multi_languages_queries = True
multi_filename_queries = False
api_based = False
_plugin_languages = {u"English (US)": "en",
u"English (UK)": "en",
u"English": "en",
u"French": "fr",
u"Brazilian": "pt-br",
u"Portuguese": "pt",
u"Español (Latinoamérica)": "es",
u"Español (España)": "es",
u"Español": "es",
u"Italian": "it",
u"Català": "ca"}
def __init__(self, config_dict=None):
super(SubsWiki, self).__init__(self._plugin_languages, config_dict, True)
self.release_pattern = re.compile("\nVersion (.+), ([0-9]+).([0-9])+ MBs")
def list(self, filenames, languages):
"""Main method to call when you want to list subtitles"""
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
filepath = filenames[0]
if not self.checkLanguages(languages):
return []
guess = guessit.guess_file_info(filepath, 'autodetect')
if guess['type'] != 'episode':
return []
# add multiple things to the release group set
release_group = set()
if 'releaseGroup' in guess:
release_group.add(guess['releaseGroup'])
else:
if 'title' in guess:
release_group.add(guess['title'])
if 'screenSize' in guess:
release_group.add(guess['screenSize'])
if 'series' not in guess or len(release_group) == 0:
return []
self.release_group = release_group # used to sort results
return self.query(guess['series'], guess['season'], guess['episodeNumber'], release_group, filepath, languages)
def query(self, name, season, episode, release_group, filepath, languages=None):
"""Make a query and returns info about found subtitles"""
sublinks = []
searchname = name.lower().replace(" ", "_")
searchurl = "%s/serie/%s/%s/%s/" % (self.server_url, searchname, season, episode)
self.logger.debug(u"Searching in %s" % searchurl)
try:
req = urllib2.Request(searchurl, headers={'User-Agent': self.user_agent})
page = urllib2.urlopen(req, timeout=self.timeout)
except urllib2.HTTPError as inst:
self.logger.info(u"Error: %s - %s" % (searchurl, inst))
return []
except urllib2.URLError as inst:
self.logger.info(u"TimeOut: %s" % inst)
return []
soup = BeautifulSoup(page.read())
for subs in soup("td", {"class": "NewsTitle"}):
sub_teams = self.listTeams([self.release_pattern.search("%s" % subs.contents[1]).group(1)], [".", "_", " ", "/", "-"])
if not release_group.intersection(sub_teams): # On wrong team
continue
self.logger.debug(u"Team from website: %s" % sub_teams)
self.logger.debug(u"Team from file: %s" % release_group)
for html_language in subs.parent.parent.findAll("td", {"class": "language"}):
sub_language = self.getRevertLanguage(html_language.string.strip())
self.logger.debug(u"Subtitle reverted language: %s" % sub_language)
if languages and not sub_language in languages: # On wrong language
continue
html_status = html_language.findNextSibling('td')
sub_status = html_status.find('strong').string.strip()
if not sub_status == 'Completed': # On not completed subtitles
continue
sub_link = html_status.findNext("td").find("a")["href"]
result = {}
result["release"] = "%s.S%.2dE%.2d.%s" % (name.replace(" ", "."), int(season), int(episode), '.'.join(sub_teams))
result["lang"] = sub_language
result["link"] = self.server_url + sub_link
result["page"] = searchurl
result["filename"] = filepath
result["plugin"] = self.getClassName()
result["teams"] = sub_teams # used to sort
sublinks.append(result)
sublinks.sort(self._cmpTeams)
return sublinks
def download(self, subtitle):
"""Main method to call when you want to download a subtitle"""
subtitleFilename = subtitle["filename"].rsplit(".", 1)[0] + self.getExtension(subtitle)
self.downloadFile(subtitle["link"], subtitleFilename)
return subtitleFilename
def listTeams(self, subteams, separators):
teams = []
for sep in separators:
subteams = self.splitTeam(subteams, sep)
return set(subteams)
def splitTeam(self, subteams, sep):
teams = []
for t in subteams:
teams += t.split(sep)
return teams
def downloadFile(self, url, filename):
"""Downloads the given url to the given filename"""
req = urllib2.Request(url, headers={'Referer': url, 'User-Agent': 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.3)'})
f = urllib2.urlopen(req)
dump = ek.ek(open, filename, "wb")
dump.write(f.read())
dump.close()
f.close()
def _cmpTeams(self, x, y):
"""Sort based on teams matching"""
return -cmp(len(x['teams'].intersection(self.release_group)), len(y['teams'].intersection(self.release_group)))
-122
View File
@@ -1,122 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import ConfigParser
import PluginBase
import traceback
import urllib
import urllib2
import xml.dom.minidom
class SubtitleSource(PluginBase.PluginBase):
site_url = 'http://www.subtitlesource.org'
site_name = 'SubtitleSource'
server_url = 'http://www.subtitlesource.org/api/%s/3.0/xmlsearch'
multi_languages_queries = True
multi_filename_queries = False
api_based = True
_plugin_languages = {"en": "English",
"sv": "Swedish",
"da": "Danish",
"fi": "Finnish",
"no": "Norwegian",
"fr": "French",
"es": "Spanish",
"is": "Icelandic"}
def __init__(self, config_dict=None):
super(SubtitleSource, self).__init__(self._plugin_languages, config_dict)
if config_dict and "subtitlesource_key" in config_dict:
self.server_url = self.server_url % config_dict["subtitlesource_key"]
else:
self.logger.error(u'SubtitleSource API Key is mandatory for this plugin')
raise Exception('SubtitleSource API Key is mandatory for this plugin')
def list(self, filenames, languages):
"""Main method to call when you want to list subtitles"""
filepath = filenames[0]
fname = self.getFileName(filepath)
subs = self.query(fname, languages)
if not subs and fname.rfind(".[") > 0:
# Try to remove the [VTV] or [EZTV] at the end of the file
teamless_filename = fname[0:fname.rfind(".[")]
subs = self.query(teamless_filename, languages)
return subs
else:
return subs
def query(self, token, languages=None):
"""Makes a query on SubtitlesSource and returns info (link, lang) about found subtitles"""
self.logger.debug(u"Local file is: %s " % token)
sublinks = []
if not languages: # langs is empty of None
languages = ["all"]
else: # parse each lang to generate the equivalent lang
languages = [self._plugin_languages[l] for l in languages if l in self._plugin_languages.keys()]
# Get the CD part of this
metaData = self.guessFileData(token)
multipart = metaData.get('part', None)
part = metaData.get('part')
if not part: # part will return None if not found using the regex
part = 1
for lang in languages:
searchurl = "%s/%s/%s/0" % (self.server_url, urllib.quote(token), lang)
self.logger.debug(u"dl'ing %s" % searchurl)
page = urllib2.urlopen(searchurl, timeout=self.timeout)
xmltree = xml.dom.minidom.parse(page)
subs = xmltree.getElementsByTagName("sub")
for sub in subs:
sublang = self.getRevertLanguage(self.getValue(sub, "language"))
if languages and not sublang in languages:
continue # The language of this sub is not wanted => Skip
if multipart and not int(self.getValue(sub, 'cd')) > 1:
continue # The subtitle is not a multipart
dllink = "http://www.subtitlesource.org/download/text/%s/%s" % (self.getValue(sub, "id"), part)
self.logger.debug(u"Link added: %s (%s)" % (dllink, sublang))
result = {}
result["release"] = self.getValue(sub, "releasename")
result["link"] = dllink
result["page"] = dllink
result["lang"] = sublang
releaseMetaData = self.guessFileData(result['release'])
teams = set(metaData['teams'])
srtTeams = set(releaseMetaData['teams'])
self.logger.debug(u"Analyzing: %s " % result['release'])
self.logger.debug(u"Local file has: %s " % metaData['teams'])
self.logger.debug(u"Remote sub has: %s " % releaseMetaData['teams'])
if result['release'].startswith(token) or (releaseMetaData['name'] == metaData['name'] and releaseMetaData['type'] == metaData['type'] and (teams.issubset(srtTeams) or srtTeams.issubset(teams))):
sublinks.append(result)
return sublinks
def download(self, subtitle):
"""Main method to call when you want to download a subtitle"""
suburl = subtitle["link"]
videofilename = subtitle["filename"]
srtfilename = videofilename.rsplit(".", 1)[0] + self.getExtension(subtitle)
self.downloadFile(suburl, srtfilename)
return srtfilename
def getValue(self, sub, tagName):
for node in sub.childNodes:
if node.nodeType == node.ELEMENT_NODE and node.tagName == tagName:
return node.childNodes[0].nodeValue
-150
View File
@@ -1,150 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from BeautifulSoup import BeautifulSoup
import guessit
import zipfile
import urllib2
import urllib
import logging
import traceback
import httplib
import re
import PluginBase
from subliminal import encodingKludge as ek
class Subtitulos(PluginBase.PluginBase):
site_url = 'http://www.subtitulos.es'
site_name = 'Subtitulos'
server_url = 'http://www.subtitulos.es'
multi_languages_queries = True
multi_filename_queries = False
api_based = False
_plugin_languages = {u"English (US)": "en",
u"English (UK)": "en",
u"English": "en",
u"French": "fr",
u"Brazilian": "pt-br",
u"Portuguese": "pt",
u"Español (Latinoamérica)": "es",
u"Español (España)": "es",
u"Español": "es",
u"Italian": "it",
u"Català": "ca"}
def __init__(self, config_dict=None):
super(Subtitulos, self).__init__(self._plugin_languages, config_dict, True)
self.release_pattern = re.compile("Versi&oacute;n (.+) ([0-9]+).([0-9])+ megabytes")
def list(self, filenames, languages):
"""Main method to call when you want to list subtitles"""
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
if not self.checkLanguages(languages):
return []
filepath = filenames[0]
guess = guessit.guess_file_info(filepath, 'autodetect')
if guess['type'] != 'episode':
return []
# add multiple things to the release group set
release_group = set()
if 'releaseGroup' in guess:
release_group.add(guess['releaseGroup'].lower())
else:
if 'title' in guess:
release_group.add(guess['title'].lower())
if 'screenSize' in guess:
release_group.add(guess['screenSize'].lower())
if 'series' not in guess or len(release_group) == 0:
return []
self.release_group = release_group # used to sort results
return self.query(guess['series'], guess['season'], guess['episodeNumber'], release_group, filepath, languages)
def query(self, name, season, episode, release_group, filepath, languages=None):
"""Make a query and returns info about found subtitles"""
sublinks = []
searchname = name.lower().replace(" ", "-")
searchurl = "%s/%s/%sx%.2d" % (self.server_url, searchname, season, episode)
self.logger.debug(u"Searching in %s" % searchurl)
try:
req = urllib2.Request(searchurl, headers={'User-Agent': self.user_agent})
page = urllib2.urlopen(req, timeout=self.timeout)
except urllib2.HTTPError as inst:
self.logger.info(u"Error: %s - %s" % (searchurl, inst))
return []
except urllib2.URLError as inst:
self.logger.info(u"TimeOut: %s" % inst)
return []
soup = BeautifulSoup(page.read())
for subs in soup("div", {"id": "version"}):
version = subs.find("p", {"class": "title-sub"})
sub_teams = self.listTeams([self.release_pattern.search("%s" % version.contents[1]).group(1).lower()], [".", "_", " ", "/"])
if not release_group.intersection(sub_teams): # On wrong team
continue
self.logger.debug(u"Team from website: %s" % sub_teams)
self.logger.debug(u"Team from file: %s" % release_group)
for html_language in subs.findAllNext("ul", {"class": "sslist"}):
sub_language = self.getRevertLanguage(html_language.findNext("li", {"class": "li-idioma"}).find("strong").contents[0].string.strip())
if languages and not sub_language in languages: # On wrong language
continue
html_status = html_language.findNext("li", {"class": "li-estado green"})
sub_status = html_status.contents[0].string.strip()
if not sub_status == 'Completado': # On not completed subtitles
continue
sub_link = html_status.findNext("span", {"class": "descargar green"}).find("a")["href"]
result = {}
result["release"] = "%s.S%.2dE%.2d.%s" % (name.replace(" ", "."), int(season), int(episode), '.'.join(sub_teams))
result["lang"] = sub_language
result["link"] = sub_link
result["page"] = searchurl
result["filename"] = filepath
result["plugin"] = self.getClassName()
result["teams"] = sub_teams # used to sort
sublinks.append(result)
sublinks.sort(self._cmpTeams)
return sublinks
def download(self, subtitle):
"""
Pass the URL of the sub and the file it matches, will unzip it
and return the path to the created file
"""
suburl = subtitle["link"]
videofilename = subtitle["filename"]
srtbasefilename = videofilename.rsplit(".", 1)[0]
srtfilename = srtbasefilename + ".srt"
self.downloadFile(suburl, srtfilename)
return srtfilename
def downloadFile(self, url, filename):
"""Downloads the given url to the given filename"""
req = urllib2.Request(url, headers={'Referer': url, 'User-Agent': 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.3)'})
f = urllib2.urlopen(req)
dump = ek.ek(open, filename, "wb")
dump.write(f.read())
dump.close()
f.close()
def _cmpTeams(self, x, y):
"""Sort based on teams matching"""
return -cmp(len(x['teams'].intersection(self.release_group)), len(y['teams'].intersection(self.release_group)))
-125
View File
@@ -1,125 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import PluginBase
import hashlib
import os
import urllib2
from subliminal import encodingKludge as ek
class TheSubDB(PluginBase.PluginBase):
site_url = 'http://thesubdb.com'
site_name = 'SubDB'
server_url = 'http://api.thesubdb.com' # for testing purpose, use http://sandbox.thesubdb.com instead
multi_languages_queries = True
multi_filename_queries = False
api_based = True
user_agent = 'SubDB/1.0 (Subliminal/0.1; https://github.com/Diaoul/subliminal)' # defined by the API
_plugin_languages = {'cs': 'cs', # the whole list is available with the API: http://sandbox.thesubdb.com/?action=languages
'da': 'da',
'de': 'de',
'en': 'en',
'fi': 'fi',
'fr': 'fr',
'hu': 'hu',
'id': 'id',
'it': 'it',
'nl': 'nl',
'no': 'no',
'pl': 'pl',
'pt': 'pt',
'ro': 'ro',
'ru': 'ru',
'sl': 'sl',
'sr': 'sr',
'sv': 'sv',
'tr': 'tr'}
def __init__(self, config_dict=None):
super(TheSubDB, self).__init__(self._plugin_languages, config_dict)
def list(self, filenames, languages):
"""Main method to call when you want to list subtitles"""
# as self.multi_filename_queries is false, we won't have multiple filenames in the list so pick the only one
# once multi-filename queries are implemented, set multi_filename_queries to true and manage a list of multiple filenames here
filepath = filenames[0]
if not ek.ek(os.path.isfile, filepath):
return []
return self.query(filepath, self.hashFile(filepath), languages)
def query(self, filepath, moviehash, languages=None):
searchurl = "%s/?action=%s&hash=%s" % (self.server_url, "search", moviehash)
self.logger.debug(u'Query URL: %s' % searchurl)
try:
req = urllib2.Request(searchurl, headers={'User-Agent': self.user_agent})
page = urllib2.urlopen(req, timeout=self.timeout)
except urllib2.HTTPError as inst:
if inst.code == 404: # no result found
return []
self.logger.error(u"Error: %s - %s" % (searchurl, inst))
return []
except urllib2.URLError as inst:
self.logger.error(u"TimeOut: %s" % inst)
return []
available_languages = page.readlines()[0].split(',')
self.logger.debug(u'Available languages: %s' % available_languages)
subs = []
for l in available_languages:
if not languages or l in languages:
result = {}
result['release'] = filepath
result['lang'] = l
result['link'] = "%s/?action=download&hash=%s&language=%s" % (self.server_url, moviehash, l)
result['page'] = result['link']
result['filename'] = filepath
result['plugin'] = self.getClassName()
subs.append(result)
return subs
def hashFile(self, name):
"""This hash function receives the filename and returns the hash code"""
readsize = 64 * 1024
with ek.ek(open, name, 'rb') as f:
size = ek.ek(os.path.getsize, name)
data = f.read(readsize)
f.seek(-readsize, os.SEEK_END)
data += f.read(readsize)
return hashlib.md5(data).hexdigest()
def download(self, subtitle):
"""Main method to call when you want to download a subtitle"""
suburl = subtitle["link"]
videofilename = subtitle["filename"]
srtfilename = videofilename.rsplit(".", 1)[0] + self.getExtension(subtitle)
self.downloadFile(suburl, srtfilename)
return srtfilename
def downloadFile(self, url, srtfilename):
"""Downloads the given url to the given filename"""
req = urllib2.Request(url)
req.add_header('User-Agent', self.user_agent)
f = urllib2.urlopen(req)
dump = open(srtfilename, "wb")
dump.write(f.read())
dump.close()
f.close()
-31
View File
@@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from Addic7ed import Addic7ed
from BierDopje import BierDopje
from OpenSubtitles import OpenSubtitles
#from Podnapisi import Podnapisi
#from SubScene import SubScene
from SubsWiki import SubsWiki
#from SubtitleSource import SubtitleSource
from Subtitulos import Subtitulos
from TheSubDB import TheSubDB
+161
View File
@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
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.
If any configuration is possible for the provider, like credentials, it must take place during instantiation.
:raise: :class:`~subliminal.exceptions.ConfigurationError` if there is a configuration error
"""
#: Supported set of :class:`~babelfish.language.Language`
languages = set()
#: Supported video types
video_types = (Episode, Movie)
#: Required hash, if any
required_hash = None
def __enter__(self):
self.initialize()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.terminate()
def initialize(self):
"""Initialize the provider.
Must be called when starting to work with the provider. This is the place for network initialization
or login operations.
.. note::
This is called automatically when entering the `with` statement
"""
raise NotImplementedError
def terminate(self):
"""Terminate the provider.
Must be called when done with the provider. This is the place for network shutdown or logout operations.
.. note::
This is called automatically when exiting the `with` statement
"""
raise NotImplementedError
@classmethod
def check(cls, video):
"""Check if the `video` can be processed.
The `video` is considered invalid if not an instance of :attr:`video_types` or if the :attr:`required_hash` is
not present in :attr:`~subliminal.video.Video.hashes` attribute of the `video`.
:param video: the video to check.
:type video: :class:`~subliminal.video.Video`
:return: `True` if the `video` is valid, `False` otherwise.
:rtype: bool
"""
if not isinstance(video, cls.video_types):
return False
if cls.required_hash is not None and cls.required_hash not in video.hashes:
return False
return True
def query(self, *args, **kwargs):
"""Query the provider for subtitles.
Arguments should match as much as possible the actual parameters for querying the provider
:return: found subtitles.
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
:raise: :class:`~subliminal.exceptions.ProviderError`
"""
raise NotImplementedError
def list_subtitles(self, video, languages):
"""List subtitles for the `video` with the given `languages`.
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.
: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`
:raise: :class:`~subliminal.exceptions.ProviderError`
"""
raise NotImplementedError
def download_subtitle(self, subtitle):
"""Download `subtitle`'s :attr:`~subliminal.subtitle.Subtitle.content`.
:param subtitle: subtitle to download.
:type subtitle: :class:`~subliminal.subtitle.Subtitle`
:raise: :class:`~subliminal.exceptions.ProviderError`
"""
raise NotImplementedError
def __repr__(self):
return '<%s [%r]>' % (self.__class__.__name__, self.video_types)
+285
View File
@@ -0,0 +1,285 @@
# -*- coding: utf-8 -*-
import logging
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, 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
@property
def id(self):
return self.download_link
def get_matches(self, video):
matches = set()
# 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')
# title
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
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):
"""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_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 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 = {'username': self.username, 'password': self.password, 'Submit': 'Log in'}
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:
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()
@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.
:return: show id per series, lower case and without quotes.
:rtype: dict
"""
# 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 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(expiration_time=SHOW_EXPIRATION_TIME)
def _search_show_id(self, series, year=None):
"""Search the show id from the `series` and `year`.
: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
"""
# addic7ed doesn't support search with quotes
series = series.replace('\'', ' ')
# 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.select('tr.epeven'):
cells = row('td')
# ignore incomplete subtitles
status = cells[5].text
if status != 'Completed':
logger.debug('Ignoring subtitle with status %s', status)
continue
# 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, video.year)
if s.language in languages and s.episode == video.episode]
def download_subtitle(self, subtitle):
# 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 DownloadLimitExceeded
subtitle.content = fix_line_ending(r.content)
+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
+294
View File
@@ -0,0 +1,294 @@
# -*- coding: utf-8 -*-
import base64
import logging
import os
import re
import zlib
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(r'^"(?P<series_name>.*)" (?P<series_title>.*)$')
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
self.movie_name = movie_name
self.movie_release_name = movie_release_name
self.movie_year = movie_year
self.movie_imdb_id = movie_imdb_id
self.series_season = series_season
self.series_episode = series_episode
self.filename = filename
@property
def id(self):
return str(self.subtitle_id)
@property
def series_name(self):
return self.series_re.match(self.movie_name).group('series_name')
@property
def series_title(self):
return self.series_re.match(self.movie_name).group('series_title')
def 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 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 |= 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 |= 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', self.movie_kind)
return matches
# imdb_id
if video.imdb_id and self.movie_imdb_id == video.imdb_id:
matches.add('imdb_id')
return matches
class OpenSubtitlesProvider(Provider):
"""OpenSubtitles Provider.
: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):
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):
logger.info('Logging out')
checked(self.server.LogOut(self.token))
self.server.close()
self.token = None
logger.debug('Logged out')
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:
criteria.append({'moviehash': hash, 'moviebytesize': str(size)})
if imdb_id:
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 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):
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):
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]))
+84
View File
@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
import logging
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
@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):
"""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 = 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 query(self, hash):
# make the query
params = {'action': 'search', 'hash': hash}
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 subtitles found')
return []
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.session.get(self.server_url, params=params, timeout=10)
r.raise_for_status()
subtitle.content = fix_line_ending(r.content)
+208
View File
@@ -0,0 +1,208 @@
# -*- coding: utf-8 -*-
import io
import logging
import re
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, 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.year = year
self.rip = rip
self.release = release
@property
def id(self):
return str(self.subtitle_id)
def get_matches(self, video):
matches = set()
# 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')
# 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
sanitize_release_group(video.release_group) in sanitize_release_group(self.release)):
matches.add('release_group')
# 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):
"""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_url = 'http://www.tvsubtitles.net/'
def initialize(self):
self.session = Session()
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
def terminate(self):
self.session.close()
@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 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
"""
# 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()
# 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
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
return show_id
@region.cache_on_arguments(expiration_time=EPISODE_EXPIRATION_TIME)
def get_episode_ids(self, show_id, season):
"""Get episode ids from the show id and the season.
:param int show_id: show id.
:param int season: season of the episode.
:return: episode ids per episode number.
:rtype: dict
"""
# 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'):
# 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 = 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, 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 []
# get the episode ids
episode_ids = self.get_episode_ids(show_id, season)
if episode not in episode_ids:
logger.error('Episode %d not found', episode)
return []
# 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, video.year) if s.language in languages]
def download_subtitle(self, subtitle):
# 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.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
+213
View File
@@ -0,0 +1,213 @@
# -*- coding: utf-8 -*-
"""
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__)
#: 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_scores(video):
"""Get the scores dict for the given `video`.
This will return either :data:`episode_scores` or :data:`movie_scores` based on the type of the `video`.
:param video: the video to compute the score against.
:type video: :class:`~subliminal.video.Video`
:return: the scores dict.
:rtype: dict
"""
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 compute_score(subtitle, video, hearing_impaired=None):
"""Compute the score of the `subtitle` against the `video` with `hearing_impaired` preference.
: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.
: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
"""
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
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])
-427
View File
@@ -1,427 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from itertools import groupby
import ConfigParser
import PluginWorker
import Queue
import locale
import logging
import mimetypes
import os
import plugins
import sys
import traceback
import locale
import encodingKludge as ek
SUPPORTED_FORMATS = 'video/x-msvideo', 'video/quicktime', 'video/x-matroska', 'video/mp4'
logger = logging.getLogger('subliminal')
SYS_ENCODING = None
try:
locale.setlocale(locale.LC_ALL, "")
SYS_ENCODING = locale.getpreferredencoding()
except (locale.Error, IOError):
pass
# for OSes that are poorly configured I'll just force UTF-8
if not SYS_ENCODING or SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
SYS_ENCODING = 'UTF-8'
class Subliminal(object):
"""Main Subliminal class"""
def __init__(self, config=True, cache_dir=True, workers=4, multi=False, force=False, max_depth=3, autostart=False, plugins_config=None, files_mode=-1):
# set default values
self.multi = multi
self.force = force
self.max_depth = max_depth
self.config = None
self.config_file = None
self.cache_dir = None
self.taskQueue = Queue.Queue()
self.resultQueue = Queue.Queue()
self._languages = None
self._plugins = self.listAPIPlugins()
self.workers = workers
self.plugins_config = plugins_config
self.files_mode = files_mode
if autostart:
self.startWorkers()
# handle configuration file preferences
try:
if config == True: # default configuration file
import xdg.BaseDirectory as bd
self.config = ConfigParser.SafeConfigParser({"languages": "", "plugins": ""})
self.config_file = ek.ek(os.path.join, bd.xdg_config_home, "subliminal", "config.ini")
if not ek.ek(os.path.exists, self.config_file): # configuration file doesn't exist, create it
self._createConfigFile()
else: # configuration file exists, load it
self._loadConfigFile()
elif config: # custom configuration file
self.config = ConfigParser.SafeConfigParser({"languages": "", "plugins": ""})
self.config_file = config
if not ek.ek(os.path.isfile, self.config_file): # custom configuration file doesn't exist, create it
self._createConfigFile()
else:
self._loadConfigFile()
except:
self.config = None
self.config_file = None
logger.error(u"Failed to use the configuration file, continue without it")
raise
# handle cache directory preferences
try:
if cache_dir == True: # default cache directory
import xdg.BaseDirectory as bd
self.cache_dir = ek.ek(os.path.join, bd.xdg_config_home, "subliminal", "cache")
if not ek.ek(os.path.exists, self.cache_dir): # cache directory doesn't exist, create it
ek.ek(os.mkdir, self.cache_dir)
logger.debug(u'Creating cache directory: %s' % self.cache_dir)
elif cache_dir: # custom configuration file
self.cache_dir = cache_dir
if not ek.ek(os.path.isdir, self.cache_dir): # custom v file doesn't exist, create it
ek.ek(os.mkdir, self.cache_dir)
logger.debug(u'Creating cache directory: %s' % self.cache_dir)
except:
self.cache_dir = None
logger.error(u"Failed to use the cache directory, continue without it")
def _loadConfigFile(self):
"""Load a configuration file specified in self.config_file"""
self.config.read(self.config_file)
self._loadLanguagesFromConfig()
self._loadPluginsFromConfig()
def _createConfigFile(self):
"""Create a configuration file specified in self.config_file"""
folder = ek.ek(os.path.dirname, self.config_file)
if not ek.ek(os.path.exists, folder):
logger.info(u"Creating folder: %s" % folder)
ek.ek(os.mkdir, folder)
# try to load a language from system
self._loadLanguageFromSystem()
self.config.set("DEFAULT", "languages", ",".join(self._languages))
self.config.set("DEFAULT", "plugins", ",".join(self._plugins))
self.config.add_section("SubtitleSource")
self.config.set("SubtitleSource", "key", "")
self._writeConfigFile()
logger.info(u"Creating configuration file: %s" % self.config_file)
logger.debug(u"Languages in created configuration file: %s" % self._languages)
logger.debug(u"Plugins in created configuration file: %s" % self._plugins)
@staticmethod
def listExistingPlugins():
"""List all possible plugins"""
return map(lambda x: x.__name__, plugins.PluginBase.PluginBase.__subclasses__())
@staticmethod
def listAPIPlugins():
"""List plugins that use API"""
return filter(Subliminal.isAPIBasedPlugin, Subliminal.listExistingPlugins())
def _writeConfigFile(self):
"""Write the configuration file"""
configfile = open(self.config_file, "w")
self.config.write(configfile)
configfile.close()
def get_languages(self):
"""Get current languages"""
return self._languages
def set_languages(self, value):
"""Set languages and save to configuration file if specified by the constructor"""
logger.debug(u"Setting languages to %s" % value)
self._languages = value
if self.config:
self._saveLanguagesToConfig()
@staticmethod
def isValidLanguage(language):
"""Check if a language is valid"""
if len(language) != 2:
logger.error(u"Language %s is not valid" % language)
return False
return True
def _saveLanguagesToConfig(self):
"""Save languages to configuration file"""
logger.debug(u"Saving languages %s to configuration file" % self._languages)
self.config.set("DEFAULT", "languages", ",".join(self._languages))
self._writeConfigFile()
def _loadLanguagesFromConfig(self):
"""Load languages from configuration file"""
configLanguages = self.config.get("DEFAULT", "languages")
logger.debug(u"Loading languages %s from configuration file" % configLanguages)
if not configLanguages:
self._languages = None
return
self._languages = filter(self.isValidLanguage, map(str.strip, configLanguages.split(",")))
def _loadLanguageFromSystem(self):
"""Load language from system"""
logger.debug(u"Loading language from system")
try:
self._languages = [locale.getdefaultlocale()[0][:2]]
logger.debug(u"Language %s loaded from system" % self._languages)
except:
logger.warning(u"Could not read language from system")
def get_plugins(self):
"""Get current plugins"""
return self._plugins
def set_plugins(self, value):
"""Set plugins and save to configuration file if specified by the constructor"""
logger.debug(u'Setting plugins to %r' % value)
self._plugins = filter(self.isValidPlugin, value)
if self.config:
self._savePluginsToConfig()
@staticmethod
def isValidPlugin(pluginName):
"""Check if a plugin is valid (exists)"""
if pluginName not in Subliminal.listExistingPlugins():
logger.error(u"Plugin %s does not exist" % pluginName)
return False
return True
@staticmethod
def isAPIBasedPlugin(pluginName):
"""Check if a plugin is API-based"""
if not getattr(plugins, pluginName).api_based:
return False
return True
def _savePluginsToConfig(self):
"""Save plugins to configuration file"""
logger.debug(u"Saving plugins %s to configuration file" % self._plugins)
self.config.set("DEFAULT", "plugins", ",".join(self._plugins))
self._writeConfigFile()
def _loadPluginsFromConfig(self):
"""Load plugins from configuration file"""
configPlugins = self.config.get("DEFAULT", "plugins")
logger.debug(u"Loading plugins %s from configuration file" % configPlugins)
self._plugins = filter(self.isValidPlugin, map(str.strip, configPlugins.split(",")))
# getters/setters for the property _languages and _plugins
languages = property(get_languages, set_languages)
plugins = property(get_plugins, set_plugins)
def deactivatePlugin(self, plugin):
"""Deactivate a plugin"""
self._plugins.remove(plugin)
if self.config:
self._savePluginsToConfig()
def activatePlugin(self, plugin):
"""Activate a plugin"""
if self.isValidPlugin(plugin):
self._plugins.append(plugin)
if self.config:
self._savePluginsToConfig()
def listSubtitles(self, entries):
"""
Searches subtitles within the active plugins and returns all found matching subtitles.
entries can be:
- filepaths
- folderpaths (N.B. internal recursive search function will be used)
- filenames
"""
search_results = []
if isinstance(entries, basestring):
entries = [ek.fixStupidEncodings(entries)]
elif not isinstance(entries, list):
raise TypeError('Entries should be a list or a string')
for e in entries:
search_results.extend(self._recursiveSearch(e))
taskCount = 0
for (l, f) in search_results:
taskCount += self.searchSubtitlesThreaded(f, l)
subtitles = []
for i in range(taskCount):
subtitles.extend(self.resultQueue.get(timeout=10))
return subtitles
@staticmethod
def arrangeSubtitles(subtitles):
"""Arrange subtitles in a handy dict by filename, language and plugin"""
arrangedSubtitles = {}
for (filename, subsByFilename) in groupby(sorted(subtitles, key=lambda x: x["filename"]), lambda x: x["filename"]):
arrangedSubtitles[filename] = {}
for (language, subsByFilenameByLanguage) in groupby(sorted(subsByFilename, key=lambda x: x["lang"]), lambda x: x["lang"]):
arrangedSubtitles[filename][language] = {}
for (plugin, subsByFilenameByLanguageByPlugin) in groupby(sorted(subsByFilenameByLanguage, key=lambda x: x["plugin"]), lambda x: x["plugin"]):
arrangedSubtitles[filename][language][plugin] = sorted(list(subsByFilenameByLanguageByPlugin))
return arrangedSubtitles
def sortSubtitlesRaw(self, subtitles):
"""Sort subtitles using user defined languages and plugins"""
return sorted(subtitles, cmp=self._cmpSubtitles)
def _cmpSubtitles(self, x, y):
"""
Compares 2 subtitles elements x and y. Returns -1 if x < y, 0 if =, 1 if >
Use filename, languages and plugin comparison
"""
filenames = sorted([x['filename'], y['filename']])
if x['filename'] != y['filename'] and filenames.index(x['filename']) < filenames(y['filename']):
return - 1
if x['filename'] != y['filename'] and filenames.index(x['filename']) > filenames(y['filename']):
return 1
if self._languages and self._languages.index(x['lang']) < self._languages.index(y['lang']):
return - 1
if self._languages and self._languages.index(x['lang']) > self._languages.index(y['lang']):
return 1
if self._plugins.index(x['plugin']) < self._plugins.index(y['plugin']):
return - 1
if self._plugins.index(x['plugin']) > self._plugins.index(y['plugin']):
return 1
return 0
def searchSubtitlesThreaded(self, filenames, languages):
"""
Makes workers search for subtitles in different languages for multiple filenames and puts the result in the result queue.
Aslo split the work in multiple tasks
When the function returns, all the results may not be available yet!
"""
logger.info(u"Searching subtitles for %s with languages %s" % (filenames, languages))
tasks = []
for pluginName in self._plugins:
try:
plugin = getattr(plugins, pluginName)(self.getConfigDict())
except:
logger.debug(traceback.print_exc())
continue
# split tasks if the plugin can't handle multi-thing queries
tasks.extend(plugin.splitTask({'task': 'list', 'plugin': pluginName, 'languages': languages, 'filenames': filenames, 'config': self.getConfigDict()}))
for t in tasks:
self.taskQueue.put(t)
return len(tasks)
def downloadSubtitlesThreaded(self, subtitles):
"""
Makes workers download subtitles and puts the result in the result queue.
When the function returns, all the results may not be available yet!
"""
# 1 task per file if not multi, 1 task per file and per language if multi
taskCount = 0
for (filename, subsByFilename) in groupby(sorted(subtitles, key=lambda x: x["filename"]), lambda x: x["filename"]):
if not self.multi:
self.taskQueue.put({'task': 'download', 'subtitle': sorted(list(subsByFilename), cmp=self._cmpSubtitles), 'config': self.getConfigDict()})
taskCount += 1
continue
for (language, subsByFilenameByLanguage) in groupby(sorted(subsByFilename, key=lambda x: x["lang"]), lambda x: x["lang"]):
self.taskQueue.put({'task': 'download', 'subtitle': sorted(list(subsByFilenameByLanguage), cmp=self._cmpSubtitles), 'config': self.getConfigDict()})
taskCount += 1
return taskCount
def downloadSubtitles(self, entries):
"""Download subtitles recursivly in entries"""
subtitles = self.listSubtitles(entries)
taskCount = self.downloadSubtitlesThreaded(subtitles)
paths = []
for i in range(taskCount):
paths.append(self.resultQueue.get(timeout=10))
return paths
def _recursiveSearch(self, entry, depth=0):
"""
Searches files in the entry
This will output a list of tuples (filename, languages)
"""
if depth > self.max_depth and self.max_depth != 0: # we do not want to search the whole file system except if max_depth = 0
return []
if ek.ek(os.path.isfile, entry): # a file? scan it
if depth != 0: # only check for valid format if recursing, trust the user
mimetypes.add_type("video/x-matroska", ".mkv")
mimetype = mimetypes.guess_type(entry)[0]
if mimetype not in SUPPORTED_FORMATS:
return []
basepath = ek.fixStupidEncodings(ek.ek(os.path.splitext, entry)[0])
# check for .xx.srt if needed
if self.multi and self.languages:
if self.force:
return [(self.languages, [ek.ek(os.path.normpath, entry)])]
needed_languages = self.languages[:]
for l in self.languages:
if ek.ek(os.path.exists, basepath + '.%s.srt' % l):
logger.info(u"Skipping language %s for file %s as it already exists. Use the --force option to force the download" % (l, entry))
needed_languages.remove(l)
if needed_languages:
return [(needed_languages, [ek.ek(os.path.normpath, entry)])]
return []
# single subtitle download: .srt
if self.force or not ek.ek(os.path.exists, basepath + '.srt'):
return [(self.languages, [ek.ek(os.path.normpath, entry)])]
if ek.ek(os.path.isdir, entry): # a dir? recurse
#TODO if hidden folder, don't keep going (how to handle windows/mac/linux ?)
files = []
for e in ek.ek(os.listdir, entry):
files.extend(self._recursiveSearch(ek.ek(os.path.join, entry, e), depth + 1))
files.sort()
grouped_files = []
for languages, group in groupby(files, lambda t: t[0]):
filenames = []
for t in group:
filenames.extend(t[1])
grouped_files.append((languages, filenames))
return grouped_files
return [] # anything else, nothing.
def startWorkers(self):
"""Create a pool of workers and start them"""
self.pool = []
for i in range(self.workers):
worker = PluginWorker.PluginWorker(self.taskQueue, self.resultQueue)
worker.start()
self.pool.append(worker)
logger.debug(u"Worker %s added to the pool" % worker.name)
def sendStopSignal(self):
"""Send a stop signal the pool of workers (poison pill)"""
logger.debug(u"Sending %d poison pills into the task queue" % self.workers)
for i in range(self.workers):
self.taskQueue.put(None)
def stopWorkers(self):
"""Stop workers using a stop signal and wait for them to terminate properly"""
self.sendStopSignal()
for worker in self.pool:
worker.join()
def getConfigDict(self):
"""Produce a dict with configuration items. Used by plugins to read configuration"""
config = {}
config['multi'] = self.multi
config['cache_dir'] = self.cache_dir
if self.config:
config['subtitlesource_key'] = self.config.get('SubtitleSource', 'key')
if self.plugins_config and 'subtitlesource_key' in self.plugins_config:
config['subtitlesource_key'] = self.plugins_config['subtitlesource_key']
config['force'] = self.force
config['files_mode'] = self.files_mode
return config
+253
View File
@@ -0,0 +1,253 @@
# -*- coding: utf-8 -*-
import codecs
import logging
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.
: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
"""
#: 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
#: URL of the web page from which the subtitle can be downloaded
self.page_link = page_link
#: 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.
:rtype: set
"""
raise NotImplementedError
def __hash__(self):
return hash(self.provider_name + '-' + self.id)
def __repr__(self):
return '<%s %r [%s]>' % (self.__class__.__name__, self.id, self.language)
def get_subtitle_path(video_path, language=None, extension='.srt'):
"""Get the subtitle path using the `video_path` and `language`.
: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_root = os.path.splitext(video_path)[0]
if language:
subtitle_root += '.' + str(language)
return subtitle_root + extension
def guess_matches(video, guess, partial=False):
"""Get matches between a `video` and a `guess`.
If a guess is `partial`, the absence information won't be counted as a match.
:param video: the video.
:type video: :class:`~subliminal.video.Video`
: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 'title' in guess and sanitize(guess['title']) == sanitize(video.series):
matches.add('series')
# 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 'episode' in guess and guess['episode'] == video.episode:
matches.add('episode')
# year
if video.year and 'year' in guess and guess['year'] == video.year:
matches.add('year')
# 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')
# resolution
if video.resolution and 'screen_size' in guess and guess['screen_size'] == video.resolution:
matches.add('resolution')
# 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 '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')
-206
View File
@@ -1,206 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2008-2011 Patrick Dessalle <patrick@dessalle.be>
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import unittest
import logging
import os
logging.basicConfig(level=logging.DEBUG, format='%(name)-24s %(levelname)-8s %(message)s')
if not os.path.exists('/tmp/subliminal/cache'):
os.mkdir('/tmp/subliminal/cache')
config = {'multi': True, 'cache_dir': '/tmp/subliminal/cache', 'subtitlesource_key': '', 'force': False}
class Addic7edListTestCase1(unittest.TestCase):
def runTest(self):
from subliminal.plugins import Addic7ed
plugin = Addic7ed(config)
results = plugin.list(["The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ["en", "fr"])
print results
assert len(results) > 0
class Addic7edListTestCase2(unittest.TestCase):
def runTest(self):
from subliminal.plugins import Addic7ed
plugin = Addic7ed(config)
results = plugin.list(["Dexter.S05E02.720p.HDTV.x264-IMMERSE.mkv"], ["en", "fr"])
print results
assert len(results) > 0
class Addic7edDownloadTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import Addic7ed
plugin = Addic7ed(config)
results = plugin.download(plugin.list(["/tmp/The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ["en", "fr"])[0])
print results
assert len(results) > 0
class BierDopjeListTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import BierDopje
plugin = BierDopje(config)
results = plugin.list(["The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ["en", "fr"])
print results
assert len(results) > 0
class BierDopjeListExceptionTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import BierDopje
plugin = BierDopje(config)
results = plugin.list(["The.Office.US.S07E08.Viewing.Party.HDTV.XviD-FQM.[VTV].avi"], ["en", "fr"])
print results
assert len(results) > 0
class BierDopjeListTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import BierDopje
plugin = BierDopje(config)
results = plugin.list(["The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ["en", "fr"])
print results
assert len(results) > 0
class OpenSubtitlesQueryTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import OpenSubtitles
plugin = OpenSubtitles()
results = plugin.query('Night.Watch.2004.CD1.DVDRiP.XViD-FiCO.avi', moviehash="09a2c497663259cb", bytesize="733589504") # http://trac.opensubtitles.org/projects/opensubtitles/wiki/XMLRPC
print results
assert len(results) > 0
class OpenSubtitlesQueryNoHashTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import OpenSubtitles
plugin = OpenSubtitles()
results = plugin.query('Night.Watch.2004.CD1.DVDRiP.XViD-FiCO.avi', languages=['en', 'fr']) # http://trac.opensubtitles.org/projects/opensubtitles/wiki/XMLRPC
print results
assert len(results) > 0
class OpenSubtitlesListTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import OpenSubtitles
plugin = OpenSubtitles()
results = plugin.download(plugin.query('/tmp/Night.Watch.2004.CD1.DVDRiP.XViD-FiCO.avi', moviehash="09a2c497663259cb", bytesize="733589504")[0]) # http://trac.opensubtitles.org/projects/opensubtitles/wiki/XMLRPC
assert len(results) > 0
class SubtitulosListTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import Subtitulos
plugin = Subtitulos()
results = plugin.list(["The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ['en', 'fr'])
print results
assert len(results) > 0
class SubtitulosDownloadTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import Subtitulos
plugin = Subtitulos()
results = plugin.download(plugin.list(["/tmp/The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ['en', 'fr'])[0])
print results
assert len(results) > 0
class TheSubDBQueryTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import TheSubDB
plugin = TheSubDB()
results = plugin.query("test.mkv", "edc1981d6459c6111fe36205b4aff6c2")
print results
assert len(results) > 0
class TheSubDBDownloadTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import TheSubDB
plugin = TheSubDB()
results = plugin.download(plugin.query("/tmp/test.mkv", "edc1981d6459c6111fe36205b4aff6c2")[0])
print results
assert len(results) > 0
class SubsWikiListTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import SubsWiki
plugin = SubsWiki()
results = plugin.list(["The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ['en', 'es'])
print results
assert len(results) > 0
class SubsWikiDownloadTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import SubsWiki
plugin = SubsWiki()
results = plugin.download(plugin.list(["/tmp/The.Big.Bang.Theory.S03E13.HDTV.XviD-2HD.mkv"], ['en', 'es'])[0])
print results
assert len(results) > 0
'''
class PodnapisiQueryTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import Podnapisi
plugin = Podnapisi()
results = plugin.query('09a2c497663259cb', ["en", "fr"])
print results
assert len(results) > 5
class SubSceneListTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import SubScene
plugin = SubScene()
results = plugin.list(["Dexter.S04E01.HDTV.XviD-NoTV.avi"], ['en', 'fr'])
print results
assert len(results) > 0, "No result could be found for Dexter.S04E01.HDTV.XviD-NoTV.avi and no languages"
class SubSceneDownloadTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import SubScene
plugin = SubScene()
results = plugin.download(plugin.list(["Dexter.S04E01.HDTV.XviD-NoTV.avi"], ['en', 'fr'])[0])
print results
assert len(results) > 0, "No result could be found for Dexter.S04E01.HDTV.XviD-NoTV.avi and no languages"
class SubtitleSourceListTestCase(unittest.TestCase):
def runTest(self):
from subliminal.plugins import SubtitleSource
plugin = SubtitleSource()
results = plugin.list(["PrisM-Inception.2010"], ['en', 'fr'])
print results
assert len(results) > 0, "No result could be found for PrisM-Inception.2010"
'''
if __name__ == "__main__":
unittest.main()
+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()
-22
View File
@@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__version__ = '0.3'
+221
View File
@@ -0,0 +1,221 @@
# -*- coding: utf-8 -*-
from __future__ import division
from datetime import datetime, timedelta
import logging
import os
from guessit import guessit
logger = logging.getLogger(__name__)
#: Video extensions
VIDEO_EXTENSIONS = ('.3g2', '.3gp', '.3gp2', '.3gpp', '.60d', '.ajp', '.asf', '.asx', '.avchd', '.avi', '.bik',
'.bix', '.box', '.cam', '.dat', '.divx', '.dmf', '.dv', '.dvr-ms', '.evo', '.flc', '.fli',
'.flic', '.flv', '.flx', '.gvi', '.gvp', '.h264', '.m1v', '.m2p', '.m2ts', '.m2v', '.m4e',
'.m4v', '.mjp', '.mjpeg', '.mjpg', '.mkv', '.moov', '.mov', '.movhd', '.movie', '.movx', '.mp4',
'.mpe', '.mpeg', '.mpg', '.mpv', '.mpv2', '.mxf', '.nsv', '.nut', '.ogg', '.ogm', '.omf', '.ps',
'.qt', '.ram', '.rm', '.rmvb', '.swf', '.ts', '.vfw', '.vid', '.video', '.viv', '.vivo', '.vob',
'.vro', '.wm', '.wmv', '.wmx', '.wrap', '.wvx', '.wx', '.x264', '.xvid')
class Video(object):
"""Base class for videos.
Represent a video, existing or not.
: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.
"""
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)
def __hash__(self):
return hash(self.name)
class Episode(Video):
"""Episode :class:`Video`.
: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.
"""
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)
#: 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 'title' not in guess or 'episode' not in guess:
raise ValueError('Insufficient data to process the guess')
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):
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`.
:param str title: title of the movie.
:param int year: year of the movie.
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
"""
def __init__(self, name, title, year=None, **kwargs):
super(Movie, self).__init__(name, **kwargs)
#: 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'], 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, %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

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