Compare commits

...

120 Commits

Author SHA1 Message Date
Antoine Bertin 6fce503814 Merge branch 'develop' 2012-06-16 08:07:20 +02:00
Antoine Bertin e873a2fbd2 Put release date in NEWS 2012-06-16 08:06:03 +02:00
Antoine Bertin 53e15969c1 Merge branch 'develop' 2012-06-16 08:02:57 +02:00
Antoine Bertin c11d83a204 Update README 2012-06-16 00:28:05 +02:00
Antoine Bertin 35d664d414 Update documentation 2012-06-16 00:25:55 +02:00
Antoine Bertin 11a33e5425 Use travis-ci sidebar in the documentation 2012-06-16 00:25:43 +02:00
Antoine Bertin 97bd82967e Update diaoul-sphinx-themes 2012-06-16 00:24:30 +02:00
Antoine Bertin ca89339042 Fix CLI 2012-06-15 23:54:25 +02:00
Antoine Bertin 07054c3f68 Fix some naming mistakes 2012-06-15 22:56:11 +02:00
Antoine Bertin 72405f5c0e Add notifications and fix installation in travis-ci 2012-06-15 22:44:41 +02:00
Antoine Bertin d5232b8a68 Add the build status in README 2012-06-15 22:43:39 +02:00
Antoine Bertin d44d032e93 Update requirements
- Add EOL in requirements.txt
- Add optional-requirements.txt for lxml
2012-06-15 22:42:32 +02:00
Antoine Bertin d7122a9c98 Clean up
- Remove blank lines
- Move the strict test in Language
- Update comments in thesubdb
- Order language_map with codes first
2012-06-15 22:40:56 +02:00
Antoine Bertin eb0b9f29f2 Avoid ValueError when a new code is found 2012-06-15 22:38:44 +02:00
Antoine Bertin a8b7763a13 Call parent __init__ in Tasks 2012-06-15 22:37:51 +02:00
Antoine Bertin 1e7fe9d216 Fix encoding issues 2012-06-15 22:37:20 +02:00
Antoine Bertin a0dbfe8c4b Refactor unittests 2012-06-15 21:00:49 +02:00
Antoine Bertin ef2571d626 Fix return type of consume_task for DownloadTasks 2012-06-15 21:00:03 +02:00
Antoine Bertin d3cde7bd05 Update travis-ci file 2012-06-15 18:40:13 +02:00
Antoine Bertin 01191d632b Update NEWS 2012-06-15 18:32:25 +02:00
Antoine Bertin 5c934766f5 Clean up
- Improve imports
- Remove unused exceptions
- Remove blank lines
- Add __all__ in modules
2012-06-15 18:31:56 +02:00
Antoine Bertin decd0e2510 Use the same return type between list_sutbitles and download_subtitles 2012-06-15 17:54:22 +02:00
Antoine Bertin cd46dad14b Add unittests for api 2012-06-15 17:47:15 +02:00
Antoine Bertin 69c5075ced Fix returned results of download_subtitles in api 2012-06-15 17:46:44 +02:00
Antoine Bertin 4da9b7080d Update services and unittests 2012-06-15 17:00:45 +02:00
Antoine Bertin be112bc091 Remove special languages from OpenSubtitles list (und, mis and mul) 2012-06-15 08:21:20 +02:00
Antoine Bertin 8389c86ce7 Rename test_set to test_set_contains in language unittests 2012-06-15 01:07:58 +02:00
Antoine Bertin fa55d32563 Update addic7ed, bierdopje and opensubtitles 2012-06-15 01:07:32 +02:00
Antoine Bertin 185cc9844c Update api, async, core and services to use the new language module 2012-06-15 01:06:31 +02:00
Antoine Bertin 705fb0d342 Init the session in the constructor of ServiceBase 2012-06-15 01:04:17 +02:00
Antoine Bertin 2ce9ac0862 Compact cache functions in ServiceBase 2012-06-15 01:03:40 +02:00
Antoine Bertin a237cd856d Use the NotImplementedError in ServiceBase 2012-06-15 01:02:34 +02:00
Antoine Bertin b1e685ffc2 Make the Video hashable 2012-06-15 01:01:39 +02:00
Antoine Bertin d19dde9843 Replace guessit.language in subtitles module 2012-06-15 01:01:01 +02:00
Antoine Bertin 74693bf747 Add language_list class and its unittests 2012-06-15 00:59:16 +02:00
Antoine Bertin 530bc7f5ff Update language module documentation and improve exception messages 2012-06-15 00:58:16 +02:00
Antoine Bertin 1b660d1e6d Fix language_set substraction operation and add a unittest 2012-06-15 00:56:31 +02:00
Antoine Bertin 34ce0d640c Fix language_set constructor with list of tuples 2012-06-15 00:53:20 +02:00
Antoine Bertin ae2c08bfbd Remove languages for special situations from the list 2012-06-15 00:51:53 +02:00
Antoine Bertin f517a683e3 Merge branch 'develop' of github.com:Diaoul/subliminal into develop 2012-06-12 23:36:02 +02:00
Antoine Bertin b512439d41 Remove some blank lines 2012-06-12 23:33:04 +02:00
Antoine Bertin c62a2a7672 Order documentation by source 2012-06-12 23:32:51 +02:00
Antoine Bertin bcd5c8f610 Add language stuff 2012-06-12 23:32:25 +02:00
Antoine Bertin a75aff65d6 Use skip in unittests 2012-06-09 23:46:36 +02:00
Antoine Bertin 9500f882d5 Replace required_parsers with required_features
Remove detection code and rely on beautifulsoup4 for that
2012-06-09 23:45:27 +02:00
Antoine Bertin 7fc5f2ef97 Update requirements 2012-06-09 19:33:54 +02:00
Antoine Bertin 5d0808a1f9 Merge branch 'develop' of git@github.com:Diaoul/subliminal.git into develop 2012-06-09 11:26:28 +02:00
Antoine Bertin 511fe7410b Update README 2012-06-08 10:57:45 +03:00
Antoine Bertin 7ed6819b03 Update unittests 2012-06-07 21:58:55 +02:00
Antoine Bertin 4709e26bd5 Update Podnapisi 2012-06-07 21:57:46 +02:00
Antoine Bertin 6e1fa561f0 Remove unused stuff in unittests 2012-06-07 18:11:09 +02:00
Antoine Bertin d7668d3573 Fix Podnapisi query method 2012-06-07 18:10:55 +02:00
Antoine Bertin 410295486a Update documentation 2012-06-07 18:09:30 +02:00
Antoine Bertin 0d63b47560 Clean up unused stuff in unittests 2012-06-03 22:05:41 +02:00
Antoine Bertin d3cb956061 Use the pythonic syntax for not in list 2012-06-03 22:04:54 +02:00
Antoine Bertin 2e00accfab Add Podnapisi service 2012-06-03 22:04:15 +02:00
Antoine Bertin c54a60097c Fix requirements 2012-06-03 15:02:23 +02:00
Antoine Bertin 58a54bce06 Merge pull request #84 from wackou/develop
Fixes for TvSubtitles and OpenSubtitles services
2012-06-03 05:15:32 -07:00
Antoine Bertin d00e6905e1 Fix python 2.6 compatibility
list.copy() is 2.7+
2012-06-03 14:10:36 +02:00
Nicolas Wack c7388c9247 Updated requirements for GuessIt 2012-05-12 17:30:30 +02:00
Nicolas Wack 5a2ab412b8 Fixed TvSubtitles download (we get back zip files, not srt ones) 2012-05-12 16:37:25 +02:00
Nicolas Wack 584acb0856 Updated OpenSubtitles' language support 2012-05-12 16:18:07 +02:00
Nicolas Wack 122f41507a Service.check_validity requires languages to be a set 2012-05-09 01:33:22 +02:00
Antoine Bertin 6a78564460 Add optional requirements to travis-ci configuration 2012-05-06 15:59:26 +02:00
Antoine Bertin 096cd5e09c Rename parser related variables. Clean up 2012-05-06 15:51:24 +02:00
Antoine Bertin cc3fa4b11a Update requirements 2012-05-06 12:08:34 +02:00
Antoine Bertin f050687487 Add automatic parser detection for BeautifulSoup 2012-05-06 11:40:12 +02:00
Antoine Bertin 4165ed0b9e Bump version to 0.6 in services 2012-05-06 11:39:23 +02:00
Antoine Bertin 3032590b8e Reorder imports 2012-05-06 11:38:36 +02:00
Antoine Bertin 113b504057 Use unicode in logging messages 2012-05-06 11:36:53 +02:00
Antoine Bertin 1a49f0b3ab Update documentation to remove references to subliminal.languages 2012-05-05 23:30:03 +02:00
Antoine Bertin a79143644b Fix import errors and be pep8 compliant 2012-05-05 23:12:02 +02:00
Antoine Bertin a2559d2d31 Add tests to setup.py and fix travis-ci support
Run tests with python setup.py test
2012-05-05 20:25:00 +02:00
Antoine Bertin 057933d737 Add support for travis-ci 2012-05-05 20:01:45 +02:00
Antoine Bertin bb16df5770 Change message when CLI does not download subtitles 2012-05-05 19:46:45 +02:00
Antoine Bertin b9c8ac23cc Fix requirements 2012-05-05 19:42:15 +02:00
Antoine Bertin bad6f77a01 Merge branch 'develop' of https://github.com/wackou/subliminal into develop 2012-05-05 19:42:07 +02:00
Nicolas Wack bb28945eba Optimized scanning for subtitles 2012-05-04 00:50:59 +02:00
Nicolas Wack 5a6ed82c5f Fixed possibly too greedy regexp 2012-05-03 23:10:21 +02:00
Nicolas Wack 055b2c7139 Fixed error message when no subtitles can be found in zipfile 2012-05-03 22:58:37 +02:00
Nicolas Wack e049eebeb0 Updated requirements for GuessIt 2012-04-28 20:56:33 +02:00
Nicolas Wack 0f3c68b7ef Removed bs4wrapper (requires BeautifulSoup>=4 now) 2012-04-28 20:23:39 +02:00
Nicolas Wack 607efff342 Merge remote-tracking branch 'olifozzy/develop' into languages_refactor 2012-04-25 02:14:14 +02:00
Nicolas Wack a88c05f7c3 Finished switching everything to guessit.Language; removed old language helper functions 2012-04-25 02:05:43 +02:00
Nicolas Wack 3aabbfde4c Subliminal now uses guessit.Language internally mostly everywhere 2012-04-24 22:25:21 +02:00
Nicolas Wack b447825a42 More flexilbe language handling 2012-04-22 03:28:12 +02:00
Nicolas Wack 2a1fb7bcb7 Unittests now clean up after themselves 2012-04-22 01:28:37 +02:00
Nicolas Wack fa7211c8b6 Added check on the size of the downloaded subtitle to make sure it is correct 2012-04-22 01:15:43 +02:00
Nicolas Wack bfa961a005 added unittests for the caching system 2012-04-21 21:43:08 +02:00
Nicolas Wack 4d9855d91c Refactored a bit unittests to use the generic EpisodeServiceTestCase 2012-04-21 20:09:04 +02:00
Nicolas Wack b09c069af8 Changed order of parameters for BierDopje.query() so that it follows the same order as the other services 2012-04-21 19:27:09 +02:00
Antoine Bertin 8fdb241a72 Update documentation 2012-04-15 23:25:14 +02:00
Nicolas Wack de781d0eec Refactored a bit the service unittests to avoid code duplication 2012-04-13 01:56:49 +02:00
Antoine Bertin 2959f52099 Fix CLI when --age is not given 2012-04-12 16:01:09 +03:00
Antoine Bertin 1e11c02006 Fix requirements for python 2.7
Add a requirement exception for previous versions of python
2012-04-11 22:24:37 +02:00
Antoine Bertin 12a58b0ca3 Bump version 2012-04-11 22:20:53 +02:00
Antoine Bertin 27f521d74e Update documentation 2012-04-11 21:15:02 +02:00
Antoine Bertin bbc3472873 Fix description for --age option in CLI 2012-04-11 20:35:47 +02:00
Antoine Bertin 680c699dfa Add --age option in CLI 2012-04-11 20:28:48 +02:00
Antoine Bertin b7ba6d51e2 Fix --workers option in CLI 2012-04-11 20:28:34 +02:00
Nicolas Wack 1f2128999f Correctly set config from a task to its cached service before consuming said task 2012-04-11 20:19:45 +02:00
olifozzy f03503b7f8 Change copyright 2012-04-11 09:53:00 +02:00
Antoine Bertin ae1b86173e Fix missing scan_filter option in async 2012-04-11 00:09:17 +02:00
Antoine Bertin e783c255b7 Add scan_filter option to filter out some paths 2012-04-10 23:48:49 +02:00
Nicolas Wack 0f86d7321f reworked caching system so that each ServiceConfig has its own Cache instance 2012-04-10 23:45:31 +02:00
Antoine Bertin f085b0fc8e Add argparse to the requirements (for python 2.6) 2012-04-10 22:07:57 +02:00
Nicolas Wack b4beba284d Caching system now has a separate cache and cache file for each service 2012-04-09 21:10:09 +02:00
Nicolas Wack c1e70e9e21 added TvSubtitles to the list of services in subliminal/core.py 2012-04-09 19:29:20 +02:00
olifozzy fca2c698fc Add unit tests 2012-04-06 15:09:35 +02:00
olifozzy 5d5b23f907 Change copyright author 2012-04-06 09:37:55 +02:00
olifozzy 2860e58830 Add Addic7ed service. 2012-04-06 01:06:26 +02:00
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
Nicolas Wack 4d06316d22 ported services to BeautifulSoup4 2012-03-05 22:02:31 +01:00
39 changed files with 2652 additions and 1157 deletions
+3
View File
@@ -31,5 +31,8 @@ pip-log.txt
.pydevproject
.settings
#Rope
.ropeproject
#Sphinx
docs/_build
+9
View File
@@ -0,0 +1,9 @@
language: python
python:
- "2.7"
install:
- pip install -r requirements.txt --use-mirrors
- pip install -r optional-requirements.txt --use-mirrors
script: python setup.py test
notifications:
irc: "irc.freenode.org#subliminal"
+15
View File
@@ -1,6 +1,21 @@
News
====
0.6.0
-----
**release date:** 2012-06-16
**WARNING:** Backward incompatible changes
* Fix --workers option in CLI
* Use a dedicated module for languages
* Use beautifulsoup4
* Improve return types
* Add scan_filter option
* Add --age option in CLI
* Add TvSubtitles service
* Add Addic7ed service
0.5.1
-----
**release date:** 2012-03-25
+10 -4
View File
@@ -1,5 +1,8 @@
Subliminal
==========
.. image:: https://secure.travis-ci.org/Diaoul/subliminal.png?branch=develop
Subliminal is a python library to search and download subtitles.
It uses video hashes and the powerful `guessit <http://guessit.readthedocs.org/>`_ library
@@ -11,11 +14,14 @@ Features
--------
Multiple subtitles services are available:
* OpenSubtitles
* TheSubDB
* Addic7ed
* BierDopje
* OpenSubtitles
* Podnapisi
* SubsWiki
* Subtitulos
* TheSubDB
* TvSubtitles
You can use main subliminal's functions with a **file path**, a **file name** or a **folder path**.
@@ -25,8 +31,8 @@ Download english subtitles::
$ subliminal -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
**************************************************
Downloaded 1 subtitles
(Episode(u'The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4'), ResultSubtitle(en, opensubtitles, 0.33, The.Big.Bang.Theory.S05E18.HDTV-LOL.srt))
Downloaded 1 subtitle(s) for 1 video(s)
The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.srt from opensubtitles
**************************************************
Module
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 34 KiB

-4
View File
@@ -1,4 +0,0 @@
<p>
<iframe src="http://markdotto.github.com/github-buttons/github-btn.html?user=Diaoul&repo=subliminal&type=watch&count=true&size=large"
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
</p>
+11 -3
View File
@@ -93,12 +93,17 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'flask'
html_theme = 'diaoul'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
html_theme_options = {'github_user': 'Diaoul',
'github_repo': 'subliminal',
'github_branch': 'develop',
'fork_me': 1,
'flattr_href': 'http://subliminal.readthedocs.org/',
'flattr_thing_url': 'http://flattr.com/thing/629842/Subliminal'}
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = ['_themes']
@@ -134,7 +139,7 @@ html_static_path = ['_static']
# Custom sidebar templates, maps document names to template names.
html_sidebars = {
'index': ['sidebar-intro.html', 'sidebar-watch.html', 'localtoc.html', 'sidebar-links.html', 'searchbox.html'],
'index': ['sidebar-intro.html', 'sidebar-watch.html', 'sidebar-travis-ci.html', 'sidebar-donate.html', 'localtoc.html', 'sidebar-links.html', 'searchbox.html'],
'**': ['localtoc.html', 'relations.html', 'sourcelink.html']
}
@@ -245,3 +250,6 @@ texinfo_documents = [
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# -- Options for autodoc -------------------------------------------------------
autodoc_member_order = 'bysource'
+15 -4
View File
@@ -8,7 +8,7 @@ combining different websites but there is no guarantee of a perfect match.
Even if OpenSubtitles has a gigantic subtitles database, you may not be able to
find a subtitle on it but you will find it elsewhere, say BierDopje. Sometimes,
it just takes some time before it shows up on a website even if already available
on another, but you don't wanna wait to watch the latest Big Bang Theory, right?
on another, but you do not want to wait to watch the latest Big Bang Theory, right?
Given this, to be reliable, subliminal has to use different :mod:`~subliminal.services`
and use a unified method to gather them all. The :class:`~subliminal.services.ServiceBase`
@@ -19,11 +19,22 @@ class will achieve this.
Languages
---------
To be able to support many languages, subliminal has a :mod:`~subliminal.languages`
module that contains utility functions and ISO languages code (639-1 and 639-2)
To be able to support many languages, subliminal makes heavy use of ISO-3166 and ISO-639
in a dedicated module.
.. automodule:: subliminal.languages
.. automodule:: subliminal.language
:members:
.. data:: subliminal.language.COUNTRIES
ISO-3166-1 countries list from `Debian package iso-codes 3.36-1 <http://packages.debian.org/fr/sid/iso-codes>`_.
Each item of this list is a tuple like ``(alpha2, alpha3, numeric, name)``
.. data:: subliminal.language.LANGUAGES
ISO-639-2 languages list from `the official source <http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt>`_.
Each item of this list is a tuple like ``(alpha3, terminologic, alpha2, name, french_name)``
Tasks
-----
+7 -4
View File
@@ -18,11 +18,14 @@ Features
--------
Multiple subtitles services are available:
* OpenSubtitles
* TheSubDB
* Addic7ed
* BierDopje
* OpenSubtitles
* Podnapisi
* SubsWiki
* Subtitulos
* TheSubDB
* TvSubtitles
You can use main subliminal's functions with a **file path**, a **file name** or a **folder path**.
@@ -32,8 +35,8 @@ Download english subtitles::
$ subliminal -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
**************************************************
Downloaded 1 subtitles
(Episode(u'The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4'), ResultSubtitle(en, opensubtitles, 0.33, The.Big.Bang.Theory.S05E18.HDTV-LOL.srt))
Downloaded 1 subtitle(s) for 1 video(s)
The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.srt from opensubtitles
**************************************************
Module
+29 -24
View File
@@ -1,5 +1,4 @@
There are 4 different ways of using subliminal and each one
is described in a dedicated section below.
There are 4 different ways of using subliminal and each one is described in a dedicated section below.
First, here are some basics
@@ -14,32 +13,38 @@ Current available services are available in the :data:`subliminal.SERVICES` vari
Languages
^^^^^^^^^
Subliminal supports multiple languages that are represented with their extended ISO 639-1 code.
The current extensions to the ISO 639-1 are:
* *po* for Brazilian Portuguese
Subliminal supports multiple languages representations based on `ISO-639 <http://en.wikipedia.org/wiki/ISO_639>`_
and `ISO-3166 <http://en.wikipedia.org/wiki/ISO_3166>`_. Any single ISO-639 string or combination of ISO-639 and
ISO-3166 is acceptable. For example, you can use ``pt-br`` for Portuguese (Brazil) or ``en`` for English.
Paths
^^^^^
All paths parameters in subliminal most commont functions can be either *a file path*,
*a file name* or a *folder path*
* File path (existing): hashes of the file will be computed and used during the search for services that supports
this functionnality.
* File name (or non-existing file path): the guessit python library will be used to guess informations and a text-based search will
be done with services.
* Folder path (containing video files): the given folder will be searched for video files using their :data:`~subliminal.videos.MIMETYPES`
and/or :data:`~subliminal.videos.EXTENSIONS`. The default maximum depth to scan is 3
* File path (existing): hashes of the file will be computed and used during the search for services that
supports this functionnality.
* File name (or non-existing file path): the guessit python library will be used to guess informations
and a text-based search will be done with services.
* Folder path (containing video files): the given folder will be searched for video files using their
:data:`~subliminal.videos.MIMETYPES` and/or :data:`~subliminal.videos.EXTENSIONS`.
The default maximum depth to scan is 3
CLI
---
Subliminal is shipped with a basic Command Line Interface that allows you to
download subtitles for one or more videos in a multi-threaded way.
Subliminal is shipped with a Command Line Interface that allows you to
download subtitles for one or more videos in a multithreaded way.
.. note::
The cache directory defaults to *~/.config/subliminal*. Even on Windows
Usage
^^^^^
You can have the documentation of the CLI using ``subliminal --help``::
usage: subliminal [-h] [-l LG] [-s NAME] [-m] [-f] [-w N] [-c] [-q | -v]
[--cache-dir DIR | --no-cache-dir] [--version]
usage: subliminal [-h] [-l LG] [-s NAME] [-m] [-f] [-w N] [-a AGE] [-c]
[-q | -v] [--cache-dir DIR | --no-cache-dir] [--version]
PATH [PATH ...]
Subtitles, faster than your thoughts
@@ -55,6 +60,8 @@ You can have the documentation of the CLI using ``subliminal --help``::
-m, --multi download multiple subtitle languages
-f, --force replace existing subtitle file
-w N, --workers N use N threads (default: 4)
-a AGE, --age AGE scan only for files newer or older (prefix with +)
than AGE (e.g. 12h, 1w2d, +3d6h)
-c, --compatibility try not to use unicode (use this if you have encoding
errors)
-q, --quiet disable output
@@ -64,9 +71,13 @@ You can have the documentation of the CLI using ``subliminal --help``::
work)
--version show program's version number and exit
.. note::
Cron job
^^^^^^^^
This CLI is well suited for automatic subtitles downloads. For example, to download english and french
subtitles for videos newer than one week under /path/to/videos/ each day at 1:00AM with a single worker,
you can use the following crontab line::
The cache directory defaults to *~/.config/subliminal*. Even on Windows
0 1 * * * user /path/to/subliminal -m -l en -l fr -w 1 -a 1w -q /path/to/videos/
Simple module use
-----------------
@@ -105,9 +116,3 @@ that takes care of that for you::
>>> with subliminal.Pool(4) as p:
... p.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
* from the command line
* basic functions :func:`~subliminal.list_subtitles` and :func:`~subliminal.download_subtitles`
* multi-threaded :class:`~subliminal.async.Pool` which implements the abovementioned functions
* using your own algorithm that produces and gather results of elementary :class:`~subliminal.tasks.Task`
+1
View File
@@ -0,0 +1 @@
lxml
+3 -2
View File
@@ -1,4 +1,5 @@
BeautifulSoup>=3.2.0
guessit>=0.2
beautifulsoup4>=4.0
guessit>=0.4.1
requests
enzyme>=0.1
html5lib
+30 -10
View File
@@ -17,9 +17,11 @@
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
import argparse
import subliminal
import datetime
import logging
import os
import re
import subliminal
import sys
@@ -29,7 +31,8 @@ def main():
parser.add_argument('-s', '--service', action='append', dest='services', help='service to use', metavar='NAME')
parser.add_argument('-m', '--multi', action='store_true', help='download multiple subtitle languages')
parser.add_argument('-f', '--force', action='store_true', help='replace existing subtitle file')
parser.add_argument('-w', '--workers', action='store', help='use N threads (default: %(default)s)', metavar='N', default=4)
parser.add_argument('-w', '--workers', action='store', help='use N threads (default: %(default)s)', metavar='N', type=int, default=4)
parser.add_argument('-a', '--age', action='store', help='scan only for files newer or older (prefix with +) than AGE (e.g. 12h, 1w2d, +3d6h)', metavar='AGE', default=None)
parser.add_argument('-c', '--compatibility', action='store_true', help='try not to use unicode (use this if you have encoding errors)')
group_verbosity = parser.add_mutually_exclusive_group()
group_verbosity.add_argument('-q', '--quiet', action='store_true', help='disable output')
@@ -51,6 +54,23 @@ def main():
if not os.path.exists(args.cache_dir):
os.mkdir(args.cache_dir)
# Create filter function
scan_filter = None
if args.age:
regex = re.compile(r'^(?P<sign>\+?)((?P<weeks>\d+?)w)?((?P<days>\d+?)d)?((?P<hours>\d+?)h)?')
parts = regex.match(args.age)
if not parts:
raise ValueError('Incorrect age format')
time_params = {}
parts = parts.groupdict()
for name, param in parts.iteritems():
if param and name != 'sign':
time_params[name] = int(param)
if parts['sign'] == '+':
scan_filter = lambda x: datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(x)) < datetime.timedelta(**time_params)
else:
scan_filter = lambda x: datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(x)) > datetime.timedelta(**time_params)
# Compatibility mode
if args.compatibility:
paths = args.paths
@@ -59,19 +79,19 @@ def main():
# Download subtitles
with subliminal.Pool(args.workers) as p:
subtitles = p.download_subtitles(paths, languages=args.languages,
services=args.services, cache_dir=args.cache_dir,
force=args.force, multi=args.multi)
results = p.download_subtitles(paths, languages=args.languages, services=args.services, cache_dir=args.cache_dir,
force=args.force, multi=args.multi, scan_filter=scan_filter)
if not subtitles:
if not results:
if not args.quiet:
sys.stderr.write('No subtitles found\n')
sys.stderr.write('No subtitles downloaded\n')
exit(1)
if not args.quiet:
print '*' * 50
print 'Downloaded %d subtitles' % len(subtitles)
for subtitle in subtitles:
print subtitle
print 'Downloaded %d subtitle(s) for %d video(s)' % (sum([len(s) for s in results.itervalues()]), len(results))
for _, subtitles in results.iteritems():
for subtitle in subtitles:
print '%s from %s' % (subtitle.path, subtitle.service)
print '*' * 50
+8 -1
View File
@@ -17,12 +17,17 @@
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
import os.path
import sys
from setuptools import setup, find_packages
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
required = ['beautifulsoup4 >= 4.0', 'guessit >= 0.4.1', 'requests', 'enzyme >= 0.1', 'html5lib']
if sys.hexversion < 0x20700f0:
required.append('argparse >= 1.1')
execfile(os.path.join(os.path.dirname(__file__), 'subliminal', 'infos.py'))
setup(name='subliminal',
version=__version__,
@@ -43,4 +48,6 @@ setup(name='subliminal',
url='https://github.com/Diaoul/subliminal',
packages=find_packages(),
scripts=['scripts/subliminal'],
install_requires=['BeautifulSoup >= 3.2.0', 'guessit >= 0.2', 'requests', 'enzyme >= 0.1'])
test_suite='tests.suite',
install_requires=required,
extras_require={'full': ['lxml']})
+17 -13
View File
@@ -18,7 +18,7 @@
from .core import (SERVICES, LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE,
MATCHING_CONFIDENCE, create_list_tasks, consume_task, create_download_tasks,
group_by_video, key_subtitles)
from .languages import list_languages
from .language import language_set, language_list, LANGUAGES
import logging
@@ -26,30 +26,32 @@ __all__ = ['list_subtitles', 'download_subtitles']
logger = logging.getLogger(__name__)
def list_subtitles(paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3):
def list_subtitles(paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None):
"""List subtitles in given paths according to the criteria
:param paths: path(s) to video file or folder
:type paths: string or list
:param list languages: languages to search for, in preferred order
:param languages: languages to search for, in preferred order
:type languages: list of :class:`~subliminal.language.Language` or string
:param list services: services to use for the search, in preferred order
:param bool force: force searching for subtitles even if some are detected
:param bool multi: search multiple languages for the same video
:param string cache_dir: path to the cache directory to use
:param int max_depth: maximum depth for scanning entries
:param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
:return: found subtitles
:rtype: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.ResultSubtitle`]
"""
services = services or SERVICES
languages = set(languages or list_languages(1))
languages = language_set(languages) if languages is not None else language_set(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
if any([not isinstance(p, unicode) for p in paths]):
logger.warning(u'Not all entries are unicode')
results = []
service_instances = {}
tasks = create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth)
tasks = create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for task in tasks:
try:
result = consume_task(task, service_instances)
@@ -61,29 +63,31 @@ def list_subtitles(paths, languages=None, services=None, force=True, multi=False
return group_by_video(results)
def download_subtitles(paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, order=None):
def download_subtitles(paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None, order=None):
"""Download subtitles in given paths according to the criteria
:param paths: path(s) to video file or folder
:type paths: string or list
:param list languages: languages to search for, in preferred order
:param languages: languages to search for, in preferred order
:type languages: list of :class:`~subliminal.language.Language` or string
:param list services: services to use for the search, in preferred order
:param bool force: force searching for subtitles even if some are detected
:param bool multi: search multiple languages for the same video
:param string cache_dir: path to the cache directory to use
:param int max_depth: maximum depth for scanning entries
:param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
:param order: preferred order for subtitles sorting
:type list: list of :data:`~subliminal.core.LANGUAGE_INDEX`, :data:`~subliminal.core.SERVICE_INDEX`, :data:`~subliminal.core.SERVICE_CONFIDENCE`, :data:`~subliminal.core.MATCHING_CONFIDENCE`
:return: found subtitles
:rtype: list of (:class:`~subliminal.videos.Video`, [:class:`~subliminal.subtitles.ResultSubtitle`])
:return: downloaded subtitles
:rtype: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.ResultSubtitle`]
"""
services = services or SERVICES
languages = languages or list_languages(1)
languages = language_list(languages) if languages is not None else language_list(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
order = order or [LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE]
subtitles_by_video = list_subtitles(paths, set(languages), services, force, multi, cache_dir, max_depth)
subtitles_by_video = list_subtitles(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for video, subtitles in subtitles_by_video.iteritems():
subtitles.sort(key=lambda s: key_subtitles(s, video, languages, services, order), reverse=True)
results = []
@@ -92,9 +96,9 @@ def download_subtitles(paths, languages=None, services=None, force=True, multi=F
for task in tasks:
try:
result = consume_task(task, service_instances)
results.append(result)
results.append((task.video, result))
except:
logger.error(u'Error consuming task %r' % task, exc_info=True)
for service_instance in service_instances.itervalues():
service_instance.terminate()
return results
return group_by_video(results)
+9 -8
View File
@@ -18,13 +18,14 @@
from .core import (consume_task, LANGUAGE_INDEX, SERVICE_INDEX,
SERVICE_CONFIDENCE, MATCHING_CONFIDENCE, SERVICES, create_list_tasks,
create_download_tasks, group_by_video, key_subtitles)
from .languages import list_languages
from .language import language_list, language_set, LANGUAGES
from .tasks import StopTask
import Queue
import logging
import threading
__all__ = ['Worker', 'Pool']
logger = logging.getLogger(__name__)
@@ -108,29 +109,29 @@ class Pool(object):
break
return results
def list_subtitles(self, paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3):
def list_subtitles(self, paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None):
"""See :meth:`subliminal.list_subtitles`"""
services = services or SERVICES
languages = set(languages or list_languages(1))
languages = language_set(languages) if languages is not None else language_set(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
if any([not isinstance(p, unicode) for p in paths]):
logger.warning(u'Not all entries are unicode')
tasks = create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth)
tasks = create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for task in tasks:
self.tasks.put(task)
self.join()
results = self.collect()
return group_by_video(results)
def download_subtitles(self, paths, languages=None, services=None, cache_dir=None, max_depth=3, force=True, multi=False, order=None):
def download_subtitles(self, paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None, order=None):
"""See :meth:`subliminal.download_subtitles`"""
services = services or SERVICES
languages = languages or list_languages(1)
languages = language_list(languages) if languages is not None else language_list(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
order = order or [LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE]
subtitles_by_video = self.list_subtitles(paths, set(languages), services, force, multi, cache_dir, max_depth)
subtitles_by_video = self.list_subtitles(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for video, subtitles in subtitles_by_video.iteritems():
subtitles.sort(key=lambda s: key_subtitles(s, video, languages, services, order), reverse=True)
tasks = create_download_tasks(subtitles_by_video, multi)
@@ -138,4 +139,4 @@ class Pool(object):
self.tasks.put(task)
self.join()
results = self.collect()
return results
return group_by_video(results)
+134
View File
@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
# Copyright 2012 Nicolas Wack <wackou@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from collections import defaultdict
from functools import wraps
import logging
import os.path
import threading
try:
import cPickle as pickle
except ImportError:
import pickle
__all__ = ['Cache', 'cachedmethod']
logger = logging.getLogger(__name__)
class Cache(object):
"""A Cache object contains cached values for methods. It can have
separate internal caches, one for each service
"""
def __init__(self, cache_dir):
self.cache_dir = cache_dir
self.cache = defaultdict(dict)
self.lock = threading.RLock()
def __del__(self):
for service_name in self.cache:
self.save(service_name)
def cache_location(self, service_name):
return os.path.join(self.cache_dir, 'subliminal_%s.cache' % service_name)
def load(self, service_name):
with self.lock:
if service_name in self.cache:
# already loaded
return
self.cache[service_name] = defaultdict(dict)
filename = self.cache_location(service_name)
logger.debug(u'Cache: loading cache from %s' % filename)
try:
self.cache[service_name] = pickle.load(open(filename, 'rb'))
except IOError:
logger.info('Cache: Cache file "%s" doesn\'t exist, creating it' % filename)
except EOFError:
logger.error('Cache: cache file "%s" is corrupted... Removing it.' % filename)
os.remove(filename)
def save(self, service_name):
filename = self.cache_location(service_name)
logger.debug(u'Cache: saving cache to %s' % filename)
with self.lock:
pickle.dump(self.cache[service_name], open(filename, 'wb'))
def clear(self, service_name):
try:
os.remove(self.cache_location(service_name))
except OSError:
pass
self.cache[service_name] = defaultdict(dict)
def cached_func_key(self, func, cls=None):
try:
cls = func.im_class
except:
pass
return ('%s.%s' % (cls.__module__, cls.__name__), func.__name__)
def function_cache(self, service_name, func):
func_key = self.cached_func_key(func)
return self.cache[service_name][func_key]
def cache_for(self, service_name, func, args, result):
# no need to lock here, dict ops are atomic
self.function_cache(service_name, func)[args] = result
def cached_value(self, service_name, func, args):
"""Raises KeyError if not found"""
# no need to lock here, dict ops are atomic
return self.function_cache(service_name, func)[args]
def cachedmethod(function):
"""Decorator to make a method use the cache.
.. note::
This can NOT be used with static functions, it has to be used on
methods of some class
"""
@wraps(function)
def cached(*args):
c = args[0].config.cache
service_name = args[0].__class__.__name__
func_key = c.cached_func_key(function, cls=args[0].__class__)
func_cache = c.cache[service_name][func_key]
# we need to remove the first element of args for the key, as it is the
# instance pointer and we don't want the cache to know which instance
# called it, it is shared among all instances of the same class
key = args[1:]
if key in func_cache:
result = func_cache[key]
logger.debug(u'Using cached value for %s(%s), returns: %s' % (func_key, key, result))
return result
result = function(*args)
# note: another thread could have already cached a value in the
# meantime, but that's ok as we prefer to keep the latest value in
# the cache
func_cache[key] = result
return result
return cached
+54 -24
View File
@@ -22,6 +22,7 @@ from .utils import get_keywords
from .videos import Episode, Movie, scan
from collections import defaultdict
from itertools import groupby
import bs4
import guessit
import logging
@@ -30,11 +31,11 @@ __all__ = ['SERVICES', 'LANGUAGE_INDEX', 'SERVICE_INDEX', 'SERVICE_CONFIDENCE',
'create_list_tasks', 'create_download_tasks', 'consume_task', 'matching_confidence',
'key_subtitles', 'group_by_video']
logger = logging.getLogger(__name__)
SERVICES = ['opensubtitles', 'bierdopje', 'subswiki', 'subtitulos', 'thesubdb']
SERVICES = ['opensubtitles', 'bierdopje', 'subswiki', 'subtitulos', 'thesubdb', 'addic7ed', 'tvsubtitles']
LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE = range(4)
def create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth):
def create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter):
"""Create a list of :class:`~subliminal.tasks.ListTask` from one or more paths using the given criteria
:param paths: path(s) to video file or folder
@@ -45,18 +46,20 @@ def create_list_tasks(paths, languages, services, force, multi, cache_dir, max_d
:param bool multi: search multiple languages for the same video
:param string cache_dir: path to the cache directory to use
:param int max_depth: maximum depth for scanning entries
:param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
:return: the created tasks
:rtype: list of :class:`~subliminal.tasks.ListTask`
"""
scan_result = []
for p in paths:
scan_result.extend(scan(p, max_depth))
scan_result.extend(scan(p, max_depth, scan_filter))
logger.debug(u'Found %d videos in %r with maximum depth %d' % (len(scan_result), paths, max_depth))
tasks = []
config = ServiceConfig(multi, cache_dir)
services = filter_services(services)
for video, detected_subtitles in scan_result:
detected_languages = set([s.language for s in detected_subtitles])
detected_languages = set(s.language for s in detected_subtitles)
wanted_languages = languages.copy()
if not force and multi:
wanted_languages -= detected_languages
@@ -70,14 +73,9 @@ def create_list_tasks(paths, languages, services, force, multi, cache_dir, max_d
for service_name in services:
mod = __import__('services.' + service_name, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
service = mod.Service
service_languages = wanted_languages & service.available_languages()
if not service_languages:
logger.debug(u'Skipping %r: none of wanted languages %r available for service %s' % (video, wanted_languages, service_name))
if not service.check_validity(video, wanted_languages):
continue
if not service.is_valid_video(video):
logger.debug(u'Skipping %r: not part of supported videos %r for service %s' % (video, service.videos, service_name))
continue
task = ListTask(video, service_languages, service_name, config)
task = ListTask(video, wanted_languages & service.languages, service_name, config)
logger.debug(u'Created task %r' % task)
tasks.append(task)
return tasks
@@ -120,7 +118,7 @@ def consume_task(task, services=None):
:type task: :class:`~subliminal.tasks.ListTask` or :class:`~subliminal.tasks.DownloadTask`
:param dict services: mapping between the service name and an instance of this service
:return: the result of the task
:rtype: list of :class:`~subliminal.subtitles.ResultSubtitle` or :class:`~subliminal.subtitles.Subtitle`
:rtype: list of :class:`~subliminal.subtitles.ResultSubtitle`
"""
if services is None:
@@ -128,21 +126,14 @@ def consume_task(task, services=None):
logger.info(u'Consuming %r' % task)
result = None
if isinstance(task, ListTask):
if task.service not in services:
mod = __import__('services.' + task.service, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
services[task.service] = mod.Service(task.config)
services[task.service].init()
subtitles = services[task.service].list(task.video, task.languages)
result = subtitles
service = get_service(services, task.service, config=task.config)
result = service.list(task.video, task.languages)
elif isinstance(task, DownloadTask):
for subtitle in task.subtitles:
if subtitle.service not in services:
mod = __import__('services.' + subtitle.service, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
services[subtitle.service] = mod.Service()
services[subtitle.service].init()
service = get_service(services, subtitle.service)
try:
services[subtitle.service].download(subtitle)
result = subtitle
service.download(subtitle)
result = [subtitle]
break
except DownloadFailedError:
logger.warning(u'Could not download subtitle %r, trying next' % subtitle)
@@ -193,6 +184,26 @@ def matching_confidence(video, subtitle):
return confidence
def get_service(services, service_name, config=None):
"""Get a service from its name in the service dict with the specified config.
If the service does not exist in the service dict, it is created and added to the dict.
:param dict services: dict where to get existing services or put created ones
:param string service_name: name of the service to get
:param config: config to use for the service
:type config: :class:`~subliminal.services.ServiceConfig` or None
:return: the corresponding service
:rtype: :class:`~subliminal.services.ServiceBase`
"""
if service_name not in services:
mod = __import__('services.' + service_name, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
services[service_name] = mod.Service()
services[service_name].init()
services[service_name].config = config
return services[service_name]
def key_subtitles(subtitle, video, languages, services, order):
"""Create a key to sort subtitle using the given order
@@ -212,6 +223,7 @@ def key_subtitles(subtitle, video, languages, services, order):
for sort_item in order:
if sort_item == LANGUAGE_INDEX:
key += '{0:03d}'.format(len(languages) - languages.index(subtitle.language) - 1)
key += '{0:01d}'.format(subtitle.language == languages[languages.index(subtitle.language)])
elif sort_item == SERVICE_INDEX:
key += '{0:02d}'.format(len(services) - services.index(subtitle.service) - 1)
elif sort_item == SERVICE_CONFIDENCE:
@@ -238,3 +250,21 @@ def group_by_video(list_results):
for video, subtitles in list_results:
result[video] += subtitles
return result
def filter_services(services):
"""Filter out services that are not available because of a missing feature
:param list services: service names to filter
:return: a copy of the initial list of service names without unavailable ones
:rtype: list
"""
filtered_services = services[:]
for service_name in services:
mod = __import__('services.' + service_name, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
service = mod.Service
if service.required_features is not None and bs4.builder_registry.lookup(*service.required_features) is None:
logger.warning(u'Service %s not available: none of available features could be used. One of %r required' % (service_name, service.required_features))
filtered_services.remove(service_name)
return filtered_services
-49
View File
@@ -22,60 +22,11 @@ class Error(Exception):
pass
class InvalidLanguageError(Error):
"""Exception raised when invalid language is submitted
Attributes:
language -- language that cause the error
"""
def __init__(self, language):
self.language = language
def __str__(self):
return self.language
class MissingLanguageError(Error):
"""Exception raised when a missing language is found
Attributes:
language -- the missing language
"""
def __init__(self, language):
self.language = language
def __str__(self):
return self.language
class InvalidServiceError(Error):
"""Exception raised when invalid service is submitted
:param string service: service that causes the error
"""
def __init__(self, service):
self.service = service
def __str__(self):
return self.service
class ServiceError(Error):
""""Exception raised by services"""
pass
class WrongTaskError(Error):
""""Exception raised when invalid task is submitted"""
pass
class DownloadFailedError(Error):
""""Exception raised when a download task has failed in service"""
pass
class UnknownVideoError(Error):
""""Exception raised when a video could not be identified"""
pass
+1 -1
View File
@@ -15,4 +15,4 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
__version__ = '0.5.1'
__version__ = '0.6.0'
File diff suppressed because it is too large Load Diff
-547
View File
@@ -1,547 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
__all__ = ['convert_language', 'list_languages', 'LANGUAGES']
def convert_language(language, to_iso, from_iso=None):
"""Convert a language into another format
:param string language: language
:param int to_iso: convert language to ISO-639-x
:param int from_iso: convert language from ISO-639-x
:return: converted language
:rtype: string
"""
if from_iso == None: # if no from_iso is given, try to guess it
if language.startswith(language[:1].upper()):
from_iso = 0
elif len(language) == 2:
from_iso = 1
elif len(language) == 3:
from_iso = 2
else:
raise ValueError('Invalid input language format')
if isinstance(language, unicode):
language = language.encode('utf-8')
converted_language = None
for language_tuple in LANGUAGES:
if language_tuple[from_iso] == language and language_tuple[to_iso]:
converted_language = language_tuple[to_iso]
break
return converted_language
def list_languages(iso):
"""List languages in the given ISO-639-x format
:param int iso: ISO-639-x format to list
:return: languages in the requested format
:rtype: list
"""
return [l[iso] for l in LANGUAGES if l[iso]]
#: ISO-639-2 languages list from http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
#: + ('Brazilian', 'po', 'pob')
LANGUAGES = [('Afar', 'aa', 'aar'),
('Abkhazian', 'ab', 'abk'),
('Achinese', '', 'ace'),
('Acoli', '', 'ach'),
('Adangme', '', 'ada'),
('Adyghe; Adygei', '', 'ady'),
('Afro-Asiatic languages', '', 'afa'),
('Afrihili', '', 'afh'),
('Afrikaans', 'af', 'afr'),
('Ainu', '', 'ain'),
('Akan', 'ak', 'aka'),
('Akkadian', '', 'akk'),
('Albanian', 'sq', 'alb'),
('Aleut', '', 'ale'),
('Algonquian languages', '', 'alg'),
('Southern Altai', '', 'alt'),
('Amharic', 'am', 'amh'),
('English, Old (ca.450-1100)', '', 'ang'),
('Angika', '', 'anp'),
('Apache languages', '', 'apa'),
('Arabic', 'ar', 'ara'),
('Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)', '', 'arc'),
('Aragonese', 'an', 'arg'),
('Armenian', 'hy', 'arm'),
('Mapudungun; Mapuche', '', 'arn'),
('Arapaho', '', 'arp'),
('Artificial languages', '', 'art'),
('Arawak', '', 'arw'),
('Assamese', 'as', 'asm'),
('Asturian; Bable; Leonese; Asturleonese', '', 'ast'),
('Athapascan languages', '', 'ath'),
('Australian languages', '', 'aus'),
('Avaric', 'av', 'ava'),
('Avestan', 'ae', 'ave'),
('Awadhi', '', 'awa'),
('Aymara', 'ay', 'aym'),
('Azerbaijani', 'az', 'aze'),
('Banda languages', '', 'bad'),
('Bamileke languages', '', 'bai'),
('Bashkir', 'ba', 'bak'),
('Baluchi', '', 'bal'),
('Bambara', 'bm', 'bam'),
('Balinese', '', 'ban'),
('Basque', 'eu', 'baq'),
('Basa', '', 'bas'),
('Baltic languages', '', 'bat'),
('Beja; Bedawiyet', '', 'bej'),
('Belarusian', 'be', 'bel'),
('Bemba', '', 'bem'),
('Bengali', 'bn', 'ben'),
('Berber languages', '', 'ber'),
('Bhojpuri', '', 'bho'),
('Bihari languages', 'bh', 'bih'),
('Bikol', '', 'bik'),
('Bini; Edo', '', 'bin'),
('Bislama', 'bi', 'bis'),
('Siksika', '', 'bla'),
('Bantu (Other)', '', 'bnt'),
('Bosnian', 'bs', 'bos'),
('Braj', '', 'bra'),
('Breton', 'br', 'bre'),
('Batak languages', '', 'btk'),
('Buriat', '', 'bua'),
('Buginese', '', 'bug'),
('Bulgarian', 'bg', 'bul'),
('Burmese', 'my', 'bur'),
('Blin; Bilin', '', 'byn'),
('Caddo', '', 'cad'),
('Central American Indian languages', '', 'cai'),
('Galibi Carib', '', 'car'),
('Catalan; Valencian', 'ca', 'cat'),
('Caucasian languages', '', 'cau'),
('Cebuano', '', 'ceb'),
('Celtic languages', '', 'cel'),
('Chamorro', 'ch', 'cha'),
('Chibcha', '', 'chb'),
('Chechen', 'ce', 'che'),
('Chagatai', '', 'chg'),
('Chinese', 'zh', 'chi'),
('Chuukese', '', 'chk'),
('Mari', '', 'chm'),
('Chinook jargon', '', 'chn'),
('Choctaw', '', 'cho'),
('Chipewyan; Dene Suline', '', 'chp'),
('Cherokee', '', 'chr'),
('Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic', 'cu', 'chu'),
('Chuvash', 'cv', 'chv'),
('Cheyenne', '', 'chy'),
('Chamic languages', '', 'cmc'),
('Coptic', '', 'cop'),
('Cornish', 'kw', 'cor'),
('Corsican', 'co', 'cos'),
('Creoles and pidgins, English based', '', 'cpe'),
('Creoles and pidgins, French-based ', '', 'cpf'),
('Creoles and pidgins, Portuguese-based ', '', 'cpp'),
('Cree', 'cr', 'cre'),
('Crimean Tatar; Crimean Turkish', '', 'crh'),
('Creoles and pidgins ', '', 'crp'),
('Kashubian', '', 'csb'),
('Cushitic languages', '', 'cus'),
('Czech', 'cs', 'cze'),
('Dakota', '', 'dak'),
('Danish', 'da', 'dan'),
('Dargwa', '', 'dar'),
('Land Dayak languages', '', 'day'),
('Delaware', '', 'del'),
('Slave (Athapascan)', '', 'den'),
('Dogrib', '', 'dgr'),
('Dinka', '', 'din'),
('Divehi; Dhivehi; Maldivian', 'dv', 'div'),
('Dogri', '', 'doi'),
('Dravidian languages', '', 'dra'),
('Lower Sorbian', '', 'dsb'),
('Duala', '', 'dua'),
('Dutch, Middle (ca.1050-1350)', '', 'dum'),
('Dutch; Flemish', 'nl', 'dut'),
('Dyula', '', 'dyu'),
('Dzongkha', 'dz', 'dzo'),
('Efik', '', 'efi'),
('Egyptian (Ancient)', '', 'egy'),
('Ekajuk', '', 'eka'),
('Elamite', '', 'elx'),
('English', 'en', 'eng'),
('English, Middle (1100-1500)', '', 'enm'),
('Esperanto', 'eo', 'epo'),
('Estonian', 'et', 'est'),
('Ewe', 'ee', 'ewe'),
('Ewondo', '', 'ewo'),
('Fang', '', 'fan'),
('Faroese', 'fo', 'fao'),
('Fanti', '', 'fat'),
('Fijian', 'fj', 'fij'),
('Filipino; Pilipino', '', 'fil'),
('Finnish', 'fi', 'fin'),
('Finno-Ugrian languages', '', 'fiu'),
('Fon', '', 'fon'),
('French', 'fr', 'fre'),
('French, Middle (ca.1400-1600)', '', 'frm'),
('French, Old (842-ca.1400)', '', 'fro'),
('Northern Frisian', '', 'frr'),
('Eastern Frisian', '', 'frs'),
('Western Frisian', 'fy', 'fry'),
('Fulah', 'ff', 'ful'),
('Friulian', '', 'fur'),
('Ga', '', 'gaa'),
('Gayo', '', 'gay'),
('Gbaya', '', 'gba'),
('Germanic languages', '', 'gem'),
('Georgian', 'ka', 'geo'),
('German', 'de', 'ger'),
('Geez', '', 'gez'),
('Gilbertese', '', 'gil'),
('Gaelic; Scottish Gaelic', 'gd', 'gla'),
('Irish', 'ga', 'gle'),
('Galician', 'gl', 'glg'),
('Manx', 'gv', 'glv'),
('German, Middle High (ca.1050-1500)', '', 'gmh'),
('German, Old High (ca.750-1050)', '', 'goh'),
('Gondi', '', 'gon'),
('Gorontalo', '', 'gor'),
('Gothic', '', 'got'),
('Grebo', '', 'grb'),
('Greek, Ancient (to 1453)', '', 'grc'),
('Greek, Modern (1453-)', 'el', 'gre'),
('Guarani', 'gn', 'grn'),
('Swiss German; Alemannic; Alsatian', '', 'gsw'),
('Gujarati', 'gu', 'guj'),
('Gwich\'in', '', 'gwi'),
('Haida', '', 'hai'),
('Haitian; Haitian Creole', 'ht', 'hat'),
('Hausa', 'ha', 'hau'),
('Hawaiian', '', 'haw'),
('Hebrew', 'he', 'heb'),
('Herero', 'hz', 'her'),
('Hiligaynon', '', 'hil'),
('Himachali languages; Western Pahari languages', '', 'him'),
('Hindi', 'hi', 'hin'),
('Hittite', '', 'hit'),
('Hmong; Mong', '', 'hmn'),
('Hiri Motu', 'ho', 'hmo'),
('Croatian', 'hr', 'hrv'),
('Upper Sorbian', '', 'hsb'),
('Hungarian', 'hu', 'hun'),
('Hupa', '', 'hup'),
('Iban', '', 'iba'),
('Igbo', 'ig', 'ibo'),
('Icelandic', 'is', 'ice'),
('Ido', 'io', 'ido'),
('Sichuan Yi; Nuosu', 'ii', 'iii'),
('Ijo languages', '', 'ijo'),
('Inuktitut', 'iu', 'iku'),
('Interlingue; Occidental', 'ie', 'ile'),
('Iloko', '', 'ilo'),
('Interlingua (International Auxiliary Language Association)', 'ia', 'ina'),
('Indic languages', '', 'inc'),
('Indonesian', 'id', 'ind'),
('Indo-European languages', '', 'ine'),
('Ingush', '', 'inh'),
('Inupiaq', 'ik', 'ipk'),
('Iranian languages', '', 'ira'),
('Iroquoian languages', '', 'iro'),
('Italian', 'it', 'ita'),
('Javanese', 'jv', 'jav'),
('Lojban', '', 'jbo'),
('Japanese', 'ja', 'jpn'),
('Judeo-Persian', '', 'jpr'),
('Judeo-Arabic', '', 'jrb'),
('Kara-Kalpak', '', 'kaa'),
('Kabyle', '', 'kab'),
('Kachin; Jingpho', '', 'kac'),
('Kalaallisut; Greenlandic', 'kl', 'kal'),
('Kamba', '', 'kam'),
('Kannada', 'kn', 'kan'),
('Karen languages', '', 'kar'),
('Kashmiri', 'ks', 'kas'),
('Kanuri', 'kr', 'kau'),
('Kawi', '', 'kaw'),
('Kazakh', 'kk', 'kaz'),
('Kabardian', '', 'kbd'),
('Khasi', '', 'kha'),
('Khoisan languages', '', 'khi'),
('Central Khmer', 'km', 'khm'),
('Khotanese; Sakan', '', 'kho'),
('Kikuyu; Gikuyu', 'ki', 'kik'),
('Kinyarwanda', 'rw', 'kin'),
('Kirghiz; Kyrgyz', 'ky', 'kir'),
('Kimbundu', '', 'kmb'),
('Konkani', '', 'kok'),
('Komi', 'kv', 'kom'),
('Kongo', 'kg', 'kon'),
('Korean', 'ko', 'kor'),
('Kosraean', '', 'kos'),
('Kpelle', '', 'kpe'),
('Karachay-Balkar', '', 'krc'),
('Karelian', '', 'krl'),
('Kru languages', '', 'kro'),
('Kurukh', '', 'kru'),
('Kuanyama; Kwanyama', 'kj', 'kua'),
('Kumyk', '', 'kum'),
('Kurdish', 'ku', 'kur'),
('Kutenai', '', 'kut'),
('Ladino', '', 'lad'),
('Lahnda', '', 'lah'),
('Lamba', '', 'lam'),
('Lao', 'lo', 'lao'),
('Latin', 'la', 'lat'),
('Latvian', 'lv', 'lav'),
('Lezghian', '', 'lez'),
('Limburgan; Limburger; Limburgish', 'li', 'lim'),
('Lingala', 'ln', 'lin'),
('Lithuanian', 'lt', 'lit'),
('Mongo', '', 'lol'),
('Lozi', '', 'loz'),
('Luxembourgish; Letzeburgesch', 'lb', 'ltz'),
('Luba-Lulua', '', 'lua'),
('Luba-Katanga', 'lu', 'lub'),
('Ganda', 'lg', 'lug'),
('Luiseno', '', 'lui'),
('Lunda', '', 'lun'),
('Luo (Kenya and Tanzania)', '', 'luo'),
('Lushai', '', 'lus'),
('Macedonian', 'mk', 'mac'),
('Madurese', '', 'mad'),
('Magahi', '', 'mag'),
('Marshallese', 'mh', 'mah'),
('Maithili', '', 'mai'),
('Makasar', '', 'mak'),
('Malayalam', 'ml', 'mal'),
('Mandingo', '', 'man'),
('Maori', 'mi', 'mao'),
('Austronesian languages', '', 'map'),
('Marathi', 'mr', 'mar'),
('Masai', '', 'mas'),
('Malay', 'ms', 'may'),
('Moksha', '', 'mdf'),
('Mandar', '', 'mdr'),
('Mende', '', 'men'),
('Irish, Middle (900-1200)', '', 'mga'),
('Mi\'kmaq; Micmac', '', 'mic'),
('Minangkabau', '', 'min'),
('Uncoded languages', '', 'mis'),
('Mon-Khmer languages', '', 'mkh'),
('Malagasy', 'mg', 'mlg'),
('Maltese', 'mt', 'mlt'),
('Manchu', '', 'mnc'),
('Manipuri', '', 'mni'),
('Manobo languages', '', 'mno'),
('Mohawk', '', 'moh'),
('Mongolian', 'mn', 'mon'),
('Mossi', '', 'mos'),
('Multiple languages', '', 'mul'),
('Munda languages', '', 'mun'),
('Creek', '', 'mus'),
('Mirandese', '', 'mwl'),
('Marwari', '', 'mwr'),
('Mayan languages', '', 'myn'),
('Erzya', '', 'myv'),
('Nahuatl languages', '', 'nah'),
('North American Indian languages', '', 'nai'),
('Neapolitan', '', 'nap'),
('Nauru', 'na', 'nau'),
('Navajo; Navaho', 'nv', 'nav'),
('Ndebele, South; South Ndebele', 'nr', 'nbl'),
('Ndebele, North; North Ndebele', 'nd', 'nde'),
('Ndonga', 'ng', 'ndo'),
('Low German; Low Saxon; German, Low; Saxon, Low', '', 'nds'),
('Nepali', 'ne', 'nep'),
('Nepal Bhasa; Newari', '', 'new'),
('Nias', '', 'nia'),
('Niger-Kordofanian languages', '', 'nic'),
('Niuean', '', 'niu'),
('Norwegian Nynorsk; Nynorsk, Norwegian', 'nn', 'nno'),
('Bokmål, Norwegian; Norwegian Bokmål', 'nb', 'nob'),
('Nogai', '', 'nog'),
('Norse, Old', '', 'non'),
('Norwegian', 'no', 'nor'),
('N\'Ko', '', 'nqo'),
('Pedi; Sepedi; Northern Sotho', '', 'nso'),
('Nubian languages', '', 'nub'),
('Classical Newari; Old Newari; Classical Nepal Bhasa', '', 'nwc'),
('Chichewa; Chewa; Nyanja', 'ny', 'nya'),
('Nyamwezi', '', 'nym'),
('Nyankole', '', 'nyn'),
('Nyoro', '', 'nyo'),
('Nzima', '', 'nzi'),
('Occitan (post 1500); Provençal', 'oc', 'oci'),
('Ojibwa', 'oj', 'oji'),
('Oriya', 'or', 'ori'),
('Oromo', 'om', 'orm'),
('Osage', '', 'osa'),
('Ossetian; Ossetic', 'os', 'oss'),
('Turkish, Ottoman (1500-1928)', '', 'ota'),
('Otomian languages', '', 'oto'),
('Papuan languages', '', 'paa'),
('Pangasinan', '', 'pag'),
('Pahlavi', '', 'pal'),
('Pampanga; Kapampangan', '', 'pam'),
('Panjabi; Punjabi', 'pa', 'pan'),
('Papiamento', '', 'pap'),
('Palauan', '', 'pau'),
('Persian, Old (ca.600-400 B.C.)', '', 'peo'),
('Persian', 'fa', 'per'),
('Philippine languages', '', 'phi'),
('Phoenician', '', 'phn'),
('Pali', 'pi', 'pli'),
('Polish', 'pl', 'pol'),
('Pohnpeian', '', 'pon'),
('Portuguese', 'pt', 'por'),
('Prakrit languages', '', 'pra'),
('Provençal, Old (to 1500)', '', 'pro'),
('Pushto; Pashto', 'ps', 'pus'),
('Reserved for local use', '', 'qaa-qtz'),
('Quechua', 'qu', 'que'),
('Rajasthani', '', 'raj'),
('Rapanui', '', 'rap'),
('Rarotongan; Cook Islands Maori', '', 'rar'),
('Romance languages', '', 'roa'),
('Romansh', 'rm', 'roh'),
('Romany', '', 'rom'),
('Romanian; Moldavian; Moldovan', 'ro', 'rum'),
('Rundi', 'rn', 'run'),
('Aromanian; Arumanian; Macedo-Romanian', '', 'rup'),
('Russian', 'ru', 'rus'),
('Sandawe', '', 'sad'),
('Sango', 'sg', 'sag'),
('Yakut', '', 'sah'),
('South American Indian (Other)', '', 'sai'),
('Salishan languages', '', 'sal'),
('Samaritan Aramaic', '', 'sam'),
('Sanskrit', 'sa', 'san'),
('Sasak', '', 'sas'),
('Santali', '', 'sat'),
('Sicilian', '', 'scn'),
('Scots', '', 'sco'),
('Selkup', '', 'sel'),
('Semitic languages', '', 'sem'),
('Irish, Old (to 900)', '', 'sga'),
('Sign Languages', '', 'sgn'),
('Shan', '', 'shn'),
('Sidamo', '', 'sid'),
('Sinhala; Sinhalese', 'si', 'sin'),
('Siouan languages', '', 'sio'),
('Sino-Tibetan languages', '', 'sit'),
('Slavic languages', '', 'sla'),
('Slovak', 'sk', 'slo'),
('Slovenian', 'sl', 'slv'),
('Southern Sami', '', 'sma'),
('Northern Sami', 'se', 'sme'),
('Sami languages', '', 'smi'),
('Lule Sami', '', 'smj'),
('Inari Sami', '', 'smn'),
('Samoan', 'sm', 'smo'),
('Skolt Sami', '', 'sms'),
('Shona', 'sn', 'sna'),
('Sindhi', 'sd', 'snd'),
('Soninke', '', 'snk'),
('Sogdian', '', 'sog'),
('Somali', 'so', 'som'),
('Songhai languages', '', 'son'),
('Sotho, Southern', 'st', 'sot'),
('Spanish; Castilian', 'es', 'spa'),
('Sardinian', 'sc', 'srd'),
('Sranan Tongo', '', 'srn'),
('Serbian', 'sr', 'srp'),
('Serer', '', 'srr'),
('Nilo-Saharan languages', '', 'ssa'),
('Swati', 'ss', 'ssw'),
('Sukuma', '', 'suk'),
('Sundanese', 'su', 'sun'),
('Susu', '', 'sus'),
('Sumerian', '', 'sux'),
('Swahili', 'sw', 'swa'),
('Swedish', 'sv', 'swe'),
('Classical Syriac', '', 'syc'),
('Syriac', '', 'syr'),
('Tahitian', 'ty', 'tah'),
('Tai languages', '', 'tai'),
('Tamil', 'ta', 'tam'),
('Tatar', 'tt', 'tat'),
('Telugu', 'te', 'tel'),
('Timne', '', 'tem'),
('Tereno', '', 'ter'),
('Tetum', '', 'tet'),
('Tajik', 'tg', 'tgk'),
('Tagalog', 'tl', 'tgl'),
('Thai', 'th', 'tha'),
('Tibetan', 'bo', 'tib'),
('Tigre', '', 'tig'),
('Tigrinya', 'ti', 'tir'),
('Tiv', '', 'tiv'),
('Tokelau', '', 'tkl'),
('Klingon; tlhIngan-Hol', '', 'tlh'),
('Tlingit', '', 'tli'),
('Tamashek', '', 'tmh'),
('Tonga (Nyasa)', '', 'tog'),
('Tonga (Tonga Islands)', 'to', 'ton'),
('Tok Pisin', '', 'tpi'),
('Tsimshian', '', 'tsi'),
('Tswana', 'tn', 'tsn'),
('Tsonga', 'ts', 'tso'),
('Turkmen', 'tk', 'tuk'),
('Tumbuka', '', 'tum'),
('Tupi languages', '', 'tup'),
('Turkish', 'tr', 'tur'),
('Altaic languages', '', 'tut'),
('Tuvalu', '', 'tvl'),
('Twi', 'tw', 'twi'),
('Tuvinian', '', 'tyv'),
('Udmurt', '', 'udm'),
('Ugaritic', '', 'uga'),
('Uighur; Uyghur', 'ug', 'uig'),
('Ukrainian', 'uk', 'ukr'),
('Umbundu', '', 'umb'),
('Undetermined', '', 'und'),
('Urdu', 'ur', 'urd'),
('Uzbek', 'uz', 'uzb'),
('Vai', '', 'vai'),
('Venda', 've', 'ven'),
('Vietnamese', 'vi', 'vie'),
('Volapük', 'vo', 'vol'),
('Votic', '', 'vot'),
('Wakashan languages', '', 'wak'),
('Walamo', '', 'wal'),
('Waray', '', 'war'),
('Washo', '', 'was'),
('Welsh', 'cy', 'wel'),
('Sorbian languages', '', 'wen'),
('Walloon', 'wa', 'wln'),
('Wolof', 'wo', 'wol'),
('Kalmyk; Oirat', '', 'xal'),
('Xhosa', 'xh', 'xho'),
('Yao', '', 'yao'),
('Yapese', '', 'yap'),
('Yiddish', 'yi', 'yid'),
('Yoruba', 'yo', 'yor'),
('Yupik languages', '', 'ypk'),
('Zapotec', '', 'zap'),
('Blissymbols; Blissymbolics; Bliss', '', 'zbl'),
('Zenaga', '', 'zen'),
('Zhuang; Chuang', 'za', 'zha'),
('Zande languages', '', 'znd'),
('Zulu', 'zu', 'zul'),
('Zuni', '', 'zun'),
('No linguistic content; Not applicable', '', 'zxx'),
('Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki', '', 'zza'),
('Brazilian', 'po', 'pob')]
+128 -86
View File
@@ -15,11 +15,15 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from ..exceptions import MissingLanguageError, DownloadFailedError
from ..cache import Cache
from ..exceptions import DownloadFailedError, ServiceError
from ..language import language_set, Language
from ..subtitles import EXTENSIONS
import logging
import os
import requests
import threading
import zipfile
__all__ = ['ServiceBase', 'ServiceConfig']
@@ -37,7 +41,7 @@ class ServiceBase(object):
server_url = ''
#: User Agent for any HTTP-based requests
user_agent = 'subliminal v0.5'
user_agent = 'subliminal v0.6'
#: Whether based on an API or not
api_based = False
@@ -45,14 +49,14 @@ class ServiceBase(object):
#: Timeout for web requests
timeout = 5
#: Lock for cache interactions
lock = threading.Lock()
#: :class:`~subliminal.language.language_set` of available languages
languages = language_set()
#: Mapping to Service's language codes and subliminal's
languages = {}
#: Map between language objects and language codes used in the service
language_map = {}
#: Whether the mapping is reverted or not
reverted_languages = False
#: Default attribute of a :class:`~subliminal.language.Language` to get with :meth:`get_code`
language_code = 'alpha2'
#: Accepted video classes (:class:`~subliminal.videos.Episode`, :class:`~subliminal.videos.Movie`, :class:`~subliminal.videos.UnknownVideo`)
videos = []
@@ -60,8 +64,12 @@ class ServiceBase(object):
#: Whether the video has to exist or not
require_video = False
#: List of required features for BeautifulSoup
required_features = None
def __init__(self, config=None):
self.config = config or ServiceConfig()
self.session = None
def __enter__(self):
self.init()
@@ -75,110 +83,104 @@ class ServiceBase(object):
logger.debug(u'Initializing %s' % self.__class__.__name__)
self.session = requests.session(timeout=10, headers={'User-Agent': self.user_agent})
def init_cache(self):
"""Initialize cache, make sure it is loaded from disk"""
if not self.config or not self.config.cache:
raise ServiceError('Cache directory is required')
self.config.cache.load(self.__class__.__name__)
def save_cache(self):
self.config.cache.save(self.__class__.__name__)
def clear_cache(self):
self.config.cache.clear(self.__class__.__name__)
def cache_for(self, func, args, result):
return self.config.cache.cache_for(self.__class__.__name__, func, args, result)
def cached_value(self, func, args):
return self.config.cache.cached_value(self.__class__.__name__, func, args)
def terminate(self):
"""Terminate connection"""
logger.debug(u'Terminating %s' % self.__class__.__name__)
def get_code(self, language):
"""Get the service code for a :class:`~subliminal.language.Language`
It uses the :data:`language_map` and if there's no match, falls back
on the :data:`language_code` attribute of the given :class:`~subliminal.language.Language`
"""
if language in self.language_map:
return self.language_map[language]
if self.language_code is None:
raise ValueError('%r has no matching code' % language)
return getattr(language, self.language_code)
def get_language(self, code):
"""Get a :class:`~subliminal.language.Language` from a service code
It uses the :data:`language_map` and if there's no match, uses the
given code as ``language`` parameter for the :class:`~subliminal.language.Language`
constructor
.. note::
A warning is emitted if the generated :class:`~subliminal.language.Language`
is "Undetermined"
"""
if code in self.language_map:
return self.language_map[code]
language = Language(code, strict=False)
if language == Language('Undetermined'):
logger.warning(u'Code %s could not be identified as a language for %s' % (code, self.__class__.__name__))
return language
def query(self, *args):
"""Make the actual query"""
pass
raise NotImplementedError()
def list(self, video, languages):
"""List subtitles"""
pass
"""List subtitles
As a service writer, you can either override this method or implement
:meth:`list_checked` instead to have the languages pre-filtered for you
"""
if not self.check_validity(video, languages):
return []
return self.list_checked(video, languages)
def list_checked(self, video, languages):
"""List subtitles without having to check parameters for validity"""
raise NotImplementedError()
def download(self, subtitle):
"""Download a subtitle"""
self.download_file(subtitle.link, subtitle.path)
@classmethod
def available_languages(cls):
"""Available languages in the Service
:return: available languages
:rtype: set
"""
if not cls.reverted_languages:
return set(cls.languages.keys())
return set(cls.languages.values())
@classmethod
def check_validity(cls, video, languages):
"""Check for video and languages validity in the Service
:param video: the video to check
:type video: :class:`~subliminal.videos.video`
:param set languages: languages to check
:param languages: languages to check
:type languages: :class:`~subliminal.language.Language`
:rtype: bool
"""
languages &= cls.available_languages()
languages = (languages & cls.languages) - language_set(['Undetermined'])
if not languages:
logger.debug(u'No language available for service %s' % cls.__class__.__name__.lower())
logger.debug(u'No language available for service %s' % cls.__name__.lower())
return False
if not cls.is_valid_video(video):
logger.debug(u'%r is not valid for service %s' % (video, cls.__class__.__name__.lower()))
if cls.require_video and not video.exists or not isinstance(video, tuple(cls.videos)):
logger.debug(u'%r is not valid for service %s' % (video, cls.__name__.lower()))
return False
return True
@classmethod
def is_valid_video(cls, video):
"""Check if video is valid in the Service
:param video: the video to check
:type video: :class:`~subliminal.videos.Video`
:rtype: bool
"""
if cls.require_video and not video.exists:
return False
if not isinstance(video, tuple(cls.videos)):
return False
return True
@classmethod
def is_valid_language(cls, language):
"""Check if language is valid in the Service
:param string language: the language to check
:rtype: bool
"""
if language in cls.available_languages():
return True
return False
@classmethod
def get_revert_language(cls, language):
"""ISO-639-1 language code from service language code
:param string language: service language code
:return: ISO-639-1 language code
:rtype: string
"""
if not cls.reverted_languages and language in cls.languages.values():
return [k for k, v in cls.languages.iteritems() if v == language][0]
if cls.reverted_languages and language in cls.languages.keys():
return cls.languages[language]
raise MissingLanguageError(language)
@classmethod
def get_language(cls, language):
"""Service language code from ISO-639-1 language code
:param string language: ISO-639-1 language code
:return: service language code
:rtype: string
"""
if not cls.reverted_languages and language in cls.languages.keys():
return cls.languages[language]
if cls.reverted_languages and language in cls.languages.values():
return [k for k, v in cls.languages.iteritems() if v == language][0]
raise MissingLanguageError(language)
def download_file(self, url, filepath):
"""Attempt to download a file and remove it in case of failure
@@ -198,6 +200,43 @@ class ServiceBase(object):
raise DownloadFailedError(str(e))
logger.debug(u'Download finished for file %s. Size: %s' % (filepath, os.path.getsize(filepath)))
def download_zip_file(self, url, filepath):
"""Attempt to download a zip file and extract any subtitle file from it, if any.
This cleans up after itself if anything fails.
:param string url: URL of the zip file to download
:param string filepath: destination path for the subtitle
"""
logger.info(u'Downloading %s' % url)
try:
zippath = filepath + '.zip'
r = self.session.get(url, headers={'Referer': url, 'User-Agent': self.user_agent})
with open(zippath, 'wb') as f:
f.write(r.content)
if not zipfile.is_zipfile(zippath):
# TODO: could check if maybe we already have a text file and
# download it directly
raise DownloadFailedError('Downloaded file is not a zip file')
zipsub = zipfile.ZipFile(zippath)
for subfile in zipsub.namelist():
if os.path.splitext(subfile)[1] in EXTENSIONS:
open(filepath, 'w').write(zipsub.open(subfile).read())
break
else:
logger.debug(u'No subtitles found in zip file')
raise DownloadFailedError('No subtitles found in zip file')
os.remove(zippath)
logger.debug(u'Download finished for file %s. Size: %s' % (filepath, os.path.getsize(filepath)))
return
except Exception as e:
logger.error(u'Download %s failed: %s' % (url, e))
if os.path.exists(zippath):
os.remove(zippath)
if os.path.exists(filepath):
os.remove(filepath)
raise DownloadFailedError(str(e))
class ServiceConfig(object):
"""Configuration for any :class:`Service`
@@ -209,6 +248,9 @@ class ServiceConfig(object):
def __init__(self, multi=False, cache_dir=None):
self.multi = multi
self.cache_dir = cache_dir
self.cache = None
if cache_dir is not None:
self.cache = Cache(cache_dir)
def __repr__(self):
return 'ServiceConfig(%r, %s)' % (self.multi, self.cache_dir)
return 'ServiceConfig(%r, %s)' % (self.multi, self.cache.cache_dir)
+154
View File
@@ -0,0 +1,154 @@
# -*- coding: utf-8 -*-
# Copyright 2012 Olivier Leveau <olifozzy@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..cache import cachedmethod
from ..language import Language, language_set
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..utils import get_keywords
from ..videos import Episode
from bs4 import BeautifulSoup
import logging
import re
logger = logging.getLogger(__name__)
def match(pattern, string):
try:
return re.search(pattern, string).group(1)
except AttributeError:
logger.debug(u'Could not match %r on %r' % (pattern, string))
return None
def matches(pattern, string):
try:
return re.search(pattern, string).group(1, 2)
except AttributeError:
logger.debug(u'Could not match %r on %r' % (pattern, string))
return None
class Addic7ed(ServiceBase):
server_url = 'http://www.addic7ed.com'
api_based = False
#TODO: Complete this
languages = language_set(['ar', 'ca', 'de', 'el', 'en', 'es', 'eu', 'fr', 'ga', 'he', 'hr', 'hu', 'it',
'pl', 'pt', 'ro', 'ru', 'se', 'pt-br'])
language_map = {'Portuguese (Brazilian)': Language('por-BR'), 'Greek': Language('gre')}
videos = [Episode]
require_video = False
required_features = ['permissive']
@cachedmethod
def get_likely_series_id(self, name):
r = self.session.get('%s/shows.php' % self.server_url)
soup = BeautifulSoup(r.content, self.required_features)
for elem in soup.find_all('h3'):
show_name = elem.a.text.lower()
show_id = int(match('show/([0-9]+)', elem.a['href']))
# we could just return the id of the queried show, but as we
# already downloaded the whole page we might as well fill in the
# information for all the shows
self.cache_for(self.get_likely_series_id, args=(show_name,), result=show_id)
return self.cached_value(self.get_likely_series_id, args=(name,))
@cachedmethod
def get_episode_url(self, series_id, season, number):
"""Get the Addic7ed id for the given episode. Raises KeyError if none
could be found
"""
# download the page of the show, contains ids for all episodes all seasons
r = self.session.get('%s/show/%d' % (self.server_url, series_id))
soup = BeautifulSoup(r.content, self.required_features)
form = soup.find('form', attrs={'name': 'multidl'})
for table in form.find_all('table'):
for row in table.find_all('tr'):
cell = row.find('td', 'MultiDldS')
if not cell:
continue
m = matches('/serie/.+/([0-9]+)/([0-9]+)/', cell.a['href'])
if not m:
continue
episode_url = cell.a['href']
season_number = int(m[0])
episode_number = int(m[1])
# we could just return the url of the queried episode, but as we
# already downloaded the whole page we might as well fill in the
# information for all the episodes of the show
self.cache_for(self.get_episode_url, args=(series_id, season_number, episode_number), result=episode_url)
# raises KeyError if not found
return self.cached_value(self.get_episode_url, args=(series_id, season, number))
# Do not cache this method in order to always check for the most recent
# subtitles
def get_sub_urls(self, episode_url):
suburls = []
r = self.session.get('%s/%s' % (self.server_url, episode_url))
epsoup = BeautifulSoup(r.content, self.required_features)
for releaseTable in epsoup.find_all('table', 'tabel95'):
releaseRow = releaseTable.find('td', 'NewsTitle')
if not releaseRow:
continue
release = releaseRow.text.strip()
for row in releaseTable.find_all('tr'):
link = row.find('a', 'buttonDownload')
if not link:
continue
if 'href' not in link.attrs or not (link['href'].startswith('/original') or link['href'].startswith('/updated')):
continue
suburl = link['href']
lang = self.get_language(row.find('td', 'language').text.strip())
result = {'suburl': suburl, 'language': lang, 'release': release}
suburls.append(result)
return suburls
def list_checked(self, video, languages):
return self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode)
def query(self, filepath, languages, keywords, series, season, episode):
logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
self.init_cache()
try:
sid = self.get_likely_series_id(series.lower())
except KeyError:
logger.debug(u'Could not find series id for %s' % series)
return []
try:
ep_url = self.get_episode_url(sid, season, episode)
except KeyError:
logger.debug(u'Could not find episode id for %s season %d episode %d' % (series, season, episode))
return []
suburls = self.get_sub_urls(ep_url)
# filter the subtitles with our queried languages
subtitles = []
for suburl in suburls:
language = suburl['language']
if language not in languages:
continue
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(),
'%s/%s' % (self.server_url, suburl['suburl']),
keywords=[suburl['release']])
subtitles.append(subtitle)
return subtitles
Service = Addic7ed
+28 -51
View File
@@ -16,13 +16,14 @@
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..cache import cachedmethod
from ..exceptions import ServiceError
from ..language import language_set
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode
from ..utils import to_unicode
import BeautifulSoup
from ..videos import Episode
from bs4 import BeautifulSoup
import logging
import os.path
import urllib
try:
import cPickle as pickle
@@ -36,30 +37,22 @@ logger = logging.getLogger(__name__)
class BierDopje(ServiceBase):
server_url = 'http://api.bierdopje.com/A2B638AC5D804C2E/'
api_based = True
languages = {'en': 'en', 'nl': 'nl'}
reverted_languages = False
languages = language_set(['eng', 'dut'])
videos = [Episode]
require_video = False
required_features = ['xml']
def __init__(self, config=None):
super(BierDopje, self).__init__(config)
self.showids = {}
if self.config and self.config.cache_dir:
self.init_cache()
def init_cache(self):
logger.debug(u'Initializing cache...')
if not self.config or not self.config.cache_dir:
raise ServiceError('Cache directory is required')
self.showids_cache = os.path.join(self.config.cache_dir, 'bierdopje_showids.cache')
if not os.path.exists(self.showids_cache):
self.save_cache()
def save_cache(self):
logger.debug(u'Saving showids to cache...')
with self.lock:
with open(self.showids_cache, 'w') as f:
pickle.dump(self.showids, f)
@cachedmethod
def get_show_id(self, series):
r = self.session.get('%sGetShowByName/%s' % (self.server_url, urllib.quote(series.lower())))
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return None
soup = BeautifulSoup(r.content, self.required_features)
if soup.status.contents[0] == 'false':
logger.debug(u'Could not find show %s' % series)
return None
return int(soup.showid.contents[0])
def load_cache(self):
logger.debug(u'Loading showids from cache...')
@@ -67,25 +60,12 @@ class BierDopje(ServiceBase):
with open(self.showids_cache, 'r') as f:
self.showids = pickle.load(f)
def query(self, season, episode, languages, filepath, tvdbid=None, series=None):
self.load_cache()
def query(self, filepath, season, episode, languages, tvdbid=None, series=None):
self.init_cache()
if series:
if series.lower() in self.showids: # from cache
request_id = self.showids[series.lower()]
logger.debug(u'Retreived showid %d for %s from cache' % (request_id, series))
else: # query to get showid
logger.debug(u'Getting showid from show name %s...' % series)
r = self.session.get('%sGetShowByName/%s' % (self.server_url, urllib.quote(series.lower())))
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
soup = BeautifulSoup.BeautifulStoneSoup(r.content)
if soup.status.contents[0] == 'false':
logger.debug(u'Could not find show %s' % series)
return []
request_id = int(soup.showid.contents[0])
self.showids[series.lower()] = request_id
self.save_cache()
request_id = self.get_show_id(series.lower())
if request_id is None:
return []
request_source = 'showid'
request_is_tvdbid = 'false'
elif tvdbid:
@@ -96,14 +76,14 @@ class BierDopje(ServiceBase):
raise ServiceError('One or more parameter missing')
subtitles = []
for language in languages:
logger.debug(u'Getting subtitles for %s %d season %d episode %d with language %s' % (request_source, request_id, season, episode, language))
r = self.session.get('%sGetAllSubsFor/%s/%s/%s/%s/%s' % (self.server_url, request_id, season, episode, language, request_is_tvdbid))
logger.debug(u'Getting subtitles for %s %d season %d episode %d with language %s' % (request_source, request_id, season, episode, language.alpha2))
r = self.session.get('%sGetAllSubsFor/%s/%s/%s/%s/%s' % (self.server_url, request_id, season, episode, language.alpha2, request_is_tvdbid))
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
soup = BeautifulSoup.BeautifulStoneSoup(r.content)
soup = BeautifulSoup(r.content, self.required_features)
if soup.status.contents[0] == 'false':
logger.debug(u'Could not find subtitles for %s %d season %d episode %d with language %s' % (request_source, request_id, season, episode, language))
logger.debug(u'Could not find subtitles for %s %d season %d episode %d with language %s' % (request_source, request_id, season, episode, language.alpha2))
continue
path = get_subtitle_path(filepath, language, self.config.multi)
for result in soup.results('result'):
@@ -112,11 +92,8 @@ class BierDopje(ServiceBase):
subtitles.append(subtitle)
return subtitles
def list(self, video, languages):
if not self.check_validity(video, languages):
return []
results = self.query(video.season, video.episode, languages, video.path or video.release, video.tvdbid, video.series)
return results
def list_checked(self, video, languages):
return self.query(video.path or video.release, video.season, video.episode, languages, video.tvdbid, video.series)
Service = BierDopje
+49 -34
View File
@@ -17,9 +17,10 @@
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..exceptions import ServiceError, DownloadFailedError
from ..language import Language, language_set
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode, Movie
from ..utils import to_unicode
from ..videos import Episode, Movie
import gzip
import logging
import os.path
@@ -32,34 +33,50 @@ logger = logging.getLogger(__name__)
class OpenSubtitles(ServiceBase):
server_url = 'http://api.opensubtitles.org/xml-rpc'
api_based = True
languages = {'aa': 'aar', 'ab': 'abk', 'af': 'afr', 'ak': 'aka', 'sq': 'alb', 'am': 'amh', 'ar': 'ara',
'an': 'arg', 'hy': 'arm', 'as': 'asm', 'av': 'ava', 'ae': 'ave', 'ay': 'aym', 'az': 'aze',
'ba': 'bak', 'bm': 'bam', 'eu': 'baq', 'be': 'bel', 'bn': 'ben', 'bh': 'bih', 'bi': 'bis',
'bs': 'bos', 'br': 'bre', 'bg': 'bul', 'my': 'bur', 'ca': 'cat', 'ch': 'cha', 'ce': 'che',
'zh': 'chi', 'cu': 'chu', 'cv': 'chv', 'kw': 'cor', 'co': 'cos', 'cr': 'cre', 'cs': 'cze',
'da': 'dan', 'dv': 'div', 'nl': 'dut', 'dz': 'dzo', 'en': 'eng', 'eo': 'epo', 'et': 'est',
'ee': 'ewe', 'fo': 'fao', 'fj': 'fij', 'fi': 'fin', 'fr': 'fre', 'fy': 'fry', 'ff': 'ful',
'ka': 'geo', 'de': 'ger', 'gd': 'gla', 'ga': 'gle', 'gl': 'glg', 'gv': 'glv', 'el': 'ell',
'gn': 'grn', 'gu': 'guj', 'ht': 'hat', 'ha': 'hau', 'he': 'heb', 'hz': 'her', 'hi': 'hin',
'ho': 'hmo', 'hr': 'hrv', 'hu': 'hun', 'ig': 'ibo', 'is': 'ice', 'io': 'ido', 'ii': 'iii',
'iu': 'iku', 'ie': 'ile', 'ia': 'ina', 'id': 'ind', 'ik': 'ipk', 'it': 'ita', 'jv': 'jav',
'ja': 'jpn', 'kl': 'kal', 'kn': 'kan', 'ks': 'kas', 'kr': 'kau', 'kk': 'kaz', 'km': 'khm',
'ki': 'kik', 'rw': 'kin', 'ky': 'kir', 'kv': 'kom', 'kg': 'kon', 'ko': 'kor', 'kj': 'kua',
'ku': 'kur', 'lo': 'lao', 'la': 'lat', 'lv': 'lav', 'li': 'lim', 'ln': 'lin', 'lt': 'lit',
'lb': 'ltz', 'lu': 'lub', 'lg': 'lug', 'mk': 'mac', 'mh': 'mah', 'ml': 'mal', 'mi': 'mao',
'mr': 'mar', 'ms': 'may', 'mg': 'mlg', 'mt': 'mlt', 'mo': 'mol', 'mn': 'mon', 'na': 'nau',
'nv': 'nav', 'nr': 'nbl', 'nd': 'nde', 'ng': 'ndo', 'ne': 'nep', 'nn': 'nno', 'nb': 'nob',
'no': 'nor', 'ny': 'nya', 'oc': 'oci', 'oj': 'oji', 'or': 'ori', 'om': 'orm', 'os': 'oss',
'pa': 'pan', 'fa': 'per', 'pi': 'pli', 'pl': 'pol', 'pt': 'por', 'ps': 'pus', 'qu': 'que',
'rm': 'roh', 'rn': 'run', 'ru': 'rus', 'sg': 'sag', 'sa': 'san', 'sr': 'scc', 'si': 'sin',
'sk': 'slo', 'sl': 'slv', 'se': 'sme', 'sm': 'smo', 'sn': 'sna', 'sd': 'snd', 'so': 'som',
'st': 'sot', 'es': 'spa', 'sc': 'srd', 'ss': 'ssw', 'su': 'sun', 'sw': 'swa', 'sv': 'swe',
'ty': 'tah', 'ta': 'tam', 'tt': 'tat', 'te': 'tel', 'tg': 'tgk', 'tl': 'tgl', 'th': 'tha',
'bo': 'tib', 'ti': 'tir', 'to': 'ton', 'tn': 'tsn', 'ts': 'tso', 'tk': 'tuk', 'tr': 'tur',
'tw': 'twi', 'ug': 'uig', 'uk': 'ukr', 'ur': 'urd', 'uz': 'uzb', 've': 'ven', 'vi': 'vie',
'vo': 'vol', 'cy': 'wel', 'wa': 'wln', 'wo': 'wol', 'xh': 'xho', 'yi': 'yid', 'yo': 'yor',
'za': 'zha', 'zu': 'zul', 'ro': 'rum', 'po': 'pob', 'un': 'unk', 'ay': 'ass'}
reverted_languages = False
# Source: http://www.opensubtitles.org/addons/export_languages.php
languages = language_set(['aar', 'abk', 'ace', 'ach', 'ada', 'ady', 'afa', 'afh', 'afr', 'ain', 'aka', 'akk',
'alb', 'ale', 'alg', 'alt', 'amh', 'ang', 'apa', 'ara', 'arc', 'arg', 'arm', 'arn',
'arp', 'art', 'arw', 'asm', 'ast', 'ath', 'aus', 'ava', 'ave', 'awa', 'aym', 'aze',
'bad', 'bai', 'bak', 'bal', 'bam', 'ban', 'baq', 'bas', 'bat', 'bej', 'bel', 'bem',
'ben', 'ber', 'bho', 'bih', 'bik', 'bin', 'bis', 'bla', 'bnt', 'bos', 'bra', 'bre',
'btk', 'bua', 'bug', 'bul', 'bur', 'byn', 'cad', 'cai', 'car', 'cat', 'cau', 'ceb',
'cel', 'cha', 'chb', 'che', 'chg', 'chi', 'chk', 'chm', 'chn', 'cho', 'chp', 'chr',
'chu', 'chv', 'chy', 'cmc', 'cop', 'cor', 'cos', 'cpe', 'cpf', 'cpp', 'cre', 'crh',
'crp', 'csb', 'cus', 'cze', 'dak', 'dan', 'dar', 'day', 'del', 'den', 'dgr', 'din',
'div', 'doi', 'dra', 'dua', 'dum', 'dut', 'dyu', 'dzo', 'efi', 'egy', 'eka', 'ell',
'elx', 'eng', 'enm', 'epo', 'est', 'ewe', 'ewo', 'fan', 'fao', 'fat', 'fij', 'fil',
'fin', 'fiu', 'fon', 'fre', 'frm', 'fro', 'fry', 'ful', 'fur', 'gaa', 'gay', 'gba',
'gem', 'geo', 'ger', 'gez', 'gil', 'gla', 'gle', 'glg', 'glv', 'gmh', 'goh', 'gon',
'gor', 'got', 'grb', 'grc', 'grn', 'guj', 'gwi', 'hai', 'hat', 'hau', 'haw', 'heb',
'her', 'hil', 'him', 'hin', 'hit', 'hmn', 'hmo', 'hrv', 'hun', 'hup', 'iba', 'ibo',
'ice', 'ido', 'iii', 'ijo', 'iku', 'ile', 'ilo', 'ina', 'inc', 'ind', 'ine', 'inh',
'ipk', 'ira', 'iro', 'ita', 'jav', 'jpn', 'jpr', 'jrb', 'kaa', 'kab', 'kac', 'kal',
'kam', 'kan', 'kar', 'kas', 'kau', 'kaw', 'kaz', 'kbd', 'kha', 'khi', 'khm', 'kho',
'kik', 'kin', 'kir', 'kmb', 'kok', 'kom', 'kon', 'kor', 'kos', 'kpe', 'krc', 'kro',
'kru', 'kua', 'kum', 'kur', 'kut', 'lad', 'lah', 'lam', 'lao', 'lat', 'lav', 'lez',
'lim', 'lin', 'lit', 'lol', 'loz', 'ltz', 'lua', 'lub', 'lug', 'lui', 'lun', 'luo',
'lus', 'mac', 'mad', 'mag', 'mah', 'mai', 'mak', 'mal', 'man', 'mao', 'map', 'mar',
'mas', 'may', 'mdf', 'mdr', 'men', 'mga', 'mic', 'min', 'mkh', 'mlg', 'mlt', 'mnc',
'mni', 'mno', 'moh', 'mon', 'mos', 'mun', 'mus', 'mwl', 'mwr', 'myn', 'myv', 'nah',
'nai', 'nap', 'nau', 'nav', 'nbl', 'nde', 'ndo', 'nds', 'nep', 'new', 'nia', 'nic',
'niu', 'nno', 'nob', 'nog', 'non', 'nor', 'nso', 'nub', 'nwc', 'nya', 'nym', 'nyn',
'nyo', 'nzi', 'oci', 'oji', 'ori', 'orm', 'osa', 'oss', 'ota', 'oto', 'paa', 'pag',
'pal', 'pam', 'pan', 'pap', 'pau', 'peo', 'per', 'phi', 'phn', 'pli', 'pol', 'pon',
'por', 'pra', 'pro', 'pus', 'que', 'raj', 'rap', 'rar', 'roa', 'roh', 'rom', 'rum',
'run', 'rup', 'rus', 'sad', 'sag', 'sah', 'sai', 'sal', 'sam', 'san', 'sas', 'sat',
'scn', 'sco', 'sel', 'sem', 'sga', 'sgn', 'shn', 'sid', 'sin', 'sio', 'sit', 'sla',
'slo', 'slv', 'sma', 'sme', 'smi', 'smj', 'smn', 'smo', 'sms', 'sna', 'snd', 'snk',
'sog', 'som', 'son', 'sot', 'spa', 'srd', 'srp', 'srr', 'ssa', 'ssw', 'suk', 'sun',
'sus', 'sux', 'swa', 'swe', 'syr', 'tah', 'tai', 'tam', 'tat', 'tel', 'tem', 'ter',
'tet', 'tgk', 'tgl', 'tha', 'tib', 'tig', 'tir', 'tiv', 'tkl', 'tlh', 'tli', 'tmh',
'tog', 'ton', 'tpi', 'tsi', 'tsn', 'tso', 'tuk', 'tum', 'tup', 'tur', 'tut', 'tvl',
'twi', 'tyv', 'udm', 'uga', 'uig', 'ukr', 'umb', 'urd', 'uzb', 'vai', 'ven', 'vie',
'vol', 'vot', 'wak', 'wal', 'war', 'was', 'wel', 'wen', 'wln', 'wol', 'xal', 'xho',
'yao', 'yap', 'yid', 'yor', 'ypk', 'zap', 'zen', 'zha', 'znd', 'zul', 'zun',
'por-BR', 'rum-MD'])
language_map = {'mol': Language('rum-MD'), 'scc': Language('srp'), 'pob': Language('por-BR'),
Language('rum-MD'): 'mol', Language('srp'): 'scc', Language('por-BR'): 'pob'}
language_code = 'alpha3'
videos = [Episode, Movie]
require_video = False
confidence_order = ['moviehash', 'imdbid', 'fulltext']
@@ -92,7 +109,7 @@ class OpenSubtitles(ServiceBase):
if not searches:
raise ServiceError('One or more parameter missing')
for search in searches:
search['sublanguageid'] = ','.join([self.get_language(l) for l in languages])
search['sublanguageid'] = ','.join(self.get_code(l) for l in languages)
logger.debug(u'Getting subtitles %r with token %s' % (searches, self.token))
results = self.server.SearchSubtitles(self.token, searches)
if not results['data']:
@@ -100,7 +117,7 @@ class OpenSubtitles(ServiceBase):
return []
subtitles = []
for result in results['data']:
language = self.get_revert_language(result['SubLanguageID'])
language = self.get_language(result['SubLanguageID'])
path = get_subtitle_path(filepath, language, self.config.multi)
confidence = 1 - float(self.confidence_order.index(result['MatchedBy'])) / float(len(self.confidence_order))
subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link=result['SubDownloadLink'],
@@ -108,9 +125,7 @@ class OpenSubtitles(ServiceBase):
subtitles.append(subtitle)
return subtitles
def list(self, video, languages):
if not self.check_validity(video, languages):
return []
def list_checked(self, video, languages):
results = []
if video.exists:
results = self.query(video.path or video.release, languages, moviehash=video.hashes['OpenSubtitles'], size=str(video.size))
+110
View File
@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..exceptions import ServiceError, DownloadFailedError
from ..language import language_set, Language
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..utils import to_unicode
from ..videos import Episode, Movie
from hashlib import md5, sha256
import logging
import xmlrpclib
logger = logging.getLogger(__name__)
class Podnapisi(ServiceBase):
server_url = 'http://ssp.podnapisi.net:8000'
api_based = True
languages = language_set(['ar', 'be', 'bg', 'bs', 'ca', 'ca', 'cs', 'da', 'de', 'el', 'en',
'es', 'et', 'fa', 'fi', 'fr', 'ga', 'he', 'hi', 'hr', 'hu', 'id',
'is', 'it', 'ja', 'ko', 'lt', 'lv', 'mk', 'ms', 'nl', 'nn', 'pl',
'pt', 'ro', 'ru', 'sk', 'sl', 'sq', 'sr', 'sv', 'th', 'tr', 'uk',
'vi', 'zh', 'es-ar', 'pt-br'])
language_map = {'jp': Language('jpn'), Language('jpn'): 'jp',
'gr': Language('gre'), Language('gre'): 'gr',
'pb': Language('por-BR'), Language('por-BR'): 'pb',
'ag': Language('spa-AR'), Language('spa-AR'): 'ag',
'cyr': Language('srp')}
videos = [Episode, Movie]
require_video = True
def __init__(self, config=None):
super(Podnapisi, self).__init__(config)
self.server = xmlrpclib.ServerProxy(self.server_url)
self.token = None
def init(self):
super(Podnapisi, self).init()
result = self.server.initiate(self.user_agent)
if result['status'] != 200:
raise ServiceError('Initiate failed')
username = 'python_subliminal'
password = sha256(md5('XWFXQ6gE5Oe12rv4qxXX').hexdigest() + result['nonce']).hexdigest()
self.token = result['session']
result = self.server.authenticate(self.token, username, password)
if result['status'] != 200:
raise ServiceError('Authenticate failed')
def terminate(self):
super(Podnapisi, self).terminate()
def query(self, filepath, languages, moviehash):
results = self.server.search(self.token, [moviehash])
if results['status'] != 200:
logger.error('Search failed with error code %d' % results['status'])
return []
if not results['results'] or not results['results'][moviehash]['subtitles']:
logger.debug(u'Could not find subtitles for %r with token %s' % (moviehash, self.token))
return []
subtitles = []
for result in results['results'][moviehash]['subtitles']:
language = self.get_language(result['lang'])
if language not in languages:
continue
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link=result['id'],
release=to_unicode(result['release']), confidence=result['weight'])
subtitles.append(subtitle)
if not subtitles:
return []
# Convert weight to confidence
max_weight = float(max([s.confidence for s in subtitles]))
min_weight = float(min([s.confidence for s in subtitles]))
for subtitle in subtitles:
if max_weight == 0 and min_weight == 0:
subtitle.confidence = 1.0
else:
subtitle.confidence = (subtitle.confidence - min_weight) / (max_weight - min_weight)
return subtitles
def list_checked(self, video, languages):
results = self.query(video.path, languages, video.hashes['OpenSubtitles'])
return results
def download(self, subtitle):
results = self.server.download(self.token, [subtitle.link])
if results['status'] != 200:
raise DownloadFailedError()
subtitle.link = 'http://www.podnapisi.net/static/podnapisi/' + results['names'][0]['filename']
self.download_file(subtitle.link, subtitle.path)
return subtitle
Service = Podnapisi
+13 -11
View File
@@ -17,10 +17,11 @@
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..exceptions import ServiceError
from ..language import language_set, Language
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode, Movie
from bs4 import BeautifulSoup
from subliminal.utils import get_keywords, split_keyword
import BeautifulSoup
import logging
import re
import urllib
@@ -32,17 +33,17 @@ logger = logging.getLogger(__name__)
class SubsWiki(ServiceBase):
server_url = 'http://www.subswiki.com'
api_based = False
languages = {u'English (US)': 'en', u'English (UK)': 'en', u'English': 'en', u'French': 'fr', u'Brazilian': 'po',
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'}
reverted_languages = True
languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'por-BR', 'por', 'spa-ES', u'spa', u'ita', u'cat'])
language_map = {u'Español': Language('spa'), u'Español (España)': Language('spa'), u'Español (Latinoamérica)': Language('spa'),
u'Català': Language('cat'), u'Brazilian': Language('por-BR'), u'English (US)': Language('eng-US'),
u'English (UK)': Language('eng-GB')}
language_code = 'name'
videos = [Episode, Movie]
require_video = False
release_pattern = re.compile('\nVersion (.+), ([0-9]+).([0-9])+ MBs')
required_features = ['permissive']
def list(self, video, languages):
if not self.check_validity(video, languages):
return []
def list_checked(self, video, languages):
results = []
if isinstance(video, Episode):
results = self.query(video.path or video.release, languages, get_keywords(video.guess), series=video.series, season=video.season, episode=video.episode)
@@ -74,7 +75,7 @@ class SubsWiki(ServiceBase):
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
soup = BeautifulSoup.BeautifulSoup(r.content)
soup = BeautifulSoup(r.content, self.required_features)
subtitles = []
for sub in soup('td', {'class': 'NewsTitle'}):
sub_keywords = split_keyword(self.release_pattern.search(sub.contents[1]).group(1).lower())
@@ -82,8 +83,8 @@ class SubsWiki(ServiceBase):
logger.debug(u'None of subtitle keywords %r in %r' % (sub_keywords, keywords))
continue
for html_language in sub.parent.parent.findAll('td', {'class': 'language'}):
language = self.get_revert_language(html_language.string.strip())
if not language in languages:
language = self.get_language(html_language.string.strip())
if language not in languages:
logger.debug(u'Language %r not in wanted languages %r' % (language, languages))
continue
html_status = html_language.findNextSibling('td')
@@ -96,4 +97,5 @@ class SubsWiki(ServiceBase):
subtitles.append(subtitle)
return subtitles
Service = SubsWiki
+18 -14
View File
@@ -16,10 +16,11 @@
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..language import language_set, Language
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode
from bs4 import BeautifulSoup
from subliminal.utils import get_keywords, split_keyword
import BeautifulSoup
import logging
import re
import unicodedata
@@ -32,19 +33,21 @@ logger = logging.getLogger(__name__)
class Subtitulos(ServiceBase):
server_url = 'http://www.subtitulos.es'
api_based = False
languages = {u'English (US)': 'en', u'English (UK)': 'en', u'English': 'en', u'French': 'fr', u'Brazilian': 'po',
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'}
reverted_languages = True
languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'por-BR', 'por', 'spa-ES', u'spa', u'ita', u'cat'])
language_map = {u'Español': Language('spa'), u'Español (España)': Language('spa'), u'Español (Latinoamérica)': Language('spa'),
u'Català': Language('cat'), u'Brazilian': Language('por-BR'), u'English (US)': Language('eng-US'),
u'English (UK)': Language('eng-GB')}
language_code = 'name'
videos = [Episode]
require_video = False
release_pattern = re.compile('Versi&oacute;n (.+) ([0-9]+).([0-9])+ megabytes')
required_features = ['permissive']
# the '.+' in the pattern for Version allows us to match both '&oacute;'
# and the 'ó' char directly. This is because now BS4 converts the html
# code chars into their equivalent unicode char
release_pattern = re.compile('Versi.+n (.+) ([0-9]+).([0-9])+ megabytes')
def list(self, video, languages):
if not self.check_validity(video, languages):
return []
results = self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode)
return results
def list_checked(self, video, languages):
return self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode)
def query(self, filepath, languages, keywords, series, season, episode):
request_series = series.lower().replace(' ', '_')
@@ -58,7 +61,7 @@ class Subtitulos(ServiceBase):
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
soup = BeautifulSoup.BeautifulSoup(r.content)
soup = BeautifulSoup(r.content, self.required_features)
subtitles = []
for sub in soup('div', {'id': 'version'}):
sub_keywords = split_keyword(self.release_pattern.search(sub.find('p', {'class': 'title-sub'}).contents[1]).group(1).lower())
@@ -66,8 +69,8 @@ class Subtitulos(ServiceBase):
logger.debug(u'None of subtitle keywords %r in %r' % (sub_keywords, keywords))
continue
for html_language in sub.findAllNext('ul', {'class': 'sslist'}):
language = self.get_revert_language(html_language.findNext('li', {'class': 'li-idioma'}).find('strong').contents[0].string.strip())
if not language in languages:
language = self.get_language(html_language.findNext('li', {'class': 'li-idioma'}).find('strong').contents[0].string.strip())
if language not in languages:
logger.debug(u'Language %r not in wanted languages %r' % (language, languages))
continue
html_status = html_language.findNext('li', {'class': 'li-estado green'})
@@ -80,4 +83,5 @@ class Subtitulos(ServiceBase):
subtitles.append(subtitle)
return subtitles
Service = Subtitulos
+12 -14
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..language import language_set
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode, Movie, UnknownVideo
import logging
@@ -25,22 +26,18 @@ logger = logging.getLogger(__name__)
class TheSubDB(ServiceBase):
server_url = 'http://api.thesubdb.com/' # for testing purpose, use http://sandbox.thesubdb.com/ instead
user_agent = 'SubDB/1.0 (subliminal/0.5; https://github.com/Diaoul/subliminal)' # defined by the API
server_url = 'http://api.thesubdb.com'
user_agent = 'SubDB/1.0 (subliminal/0.6; https://github.com/Diaoul/subliminal)'
api_based = True
languages = {'af': 'af', 'cs': 'cs', 'da': 'da', 'de': 'de', 'en': 'en', 'es': 'es', 'fi': 'fi',
'fr': 'fr', 'hu': 'hu', 'id': 'id', 'it': 'it', 'la': 'la', 'nl': 'nl', 'no': 'no',
'oc': 'oc', 'pl': 'pl', 'pt': 'pt', 'ro': 'ro', 'ru': 'ru', 'sl': 'sl', 'sr': 'sr',
'sv': 'sv', 'tr': 'tr'} # list available with the API at http://sandbox.thesubdb.com/?action=languages
reverted_languages = False
# Source: http://api.thesubdb.com/?action=languages
languages = language_set(['af', 'cs', 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'id', 'it',
'la', 'nl', 'no', 'oc', 'pl', 'pt', 'ro', 'ru', 'sl', 'sr', 'sv',
'tr'])
videos = [Movie, Episode, UnknownVideo]
require_video = True
def list(self, video, languages):
if not self.check_validity(video, languages):
return []
results = self.query(video.path, video.hashes['TheSubDB'], languages)
return results
def list_checked(self, video, languages):
return self.query(video.path, video.hashes['TheSubDB'], languages)
def query(self, filepath, moviehash, languages):
r = self.session.get(self.server_url, params={'action': 'search', 'hash': moviehash})
@@ -50,7 +47,7 @@ class TheSubDB(ServiceBase):
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
available_languages = set([self.get_revert_language(l) for l in r.content.split(',')])
available_languages = language_set(r.content.split(','))
languages &= available_languages
if not languages:
logger.debug(u'Could not find subtitles for hash %s with languages %r (only %r available)' % (moviehash, languages, available_languages))
@@ -58,8 +55,9 @@ class TheSubDB(ServiceBase):
subtitles = []
for language in languages:
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link='%s?action=download&hash=%s&language=%s' % (self.server_url, moviehash, self.get_language(language)))
subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), '%s?action=download&hash=%s&language=%s' % (self.server_url, moviehash, language.alpha2))
subtitles.append(subtitle)
return subtitles
Service = TheSubDB
+141
View File
@@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
# Copyright 2012 Nicolas Wack <wackou@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import ServiceBase
from ..cache import cachedmethod
from ..language import language_set, Language
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..utils import get_keywords
from ..videos import Episode
from bs4 import BeautifulSoup
import logging
import re
logger = logging.getLogger(__name__)
def match(pattern, string):
try:
return re.search(pattern, string).group(1)
except AttributeError:
logger.debug(u'Could not match %r on %r' % (pattern, string))
return None
class TvSubtitles(ServiceBase):
server_url = 'http://www.tvsubtitles.net'
api_based = False
languages = language_set(['ar', 'bg', 'cs', 'da', 'de', 'el', 'en', 'es', 'fi', 'fr', 'hu',
'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ro', 'ru', 'sv', 'tr', 'uk',
'zh', 'pt-br'])
#TODO: Find more exceptions
language_map = {'gr': Language('gre'), 'cz': Language('cze'), 'ua': Language('ukr')}
videos = [Episode]
require_video = False
required_features = ['permissive']
@cachedmethod
def get_likely_series_id(self, name):
r = self.session.post('%s/search.php' % self.server_url, data={'q': name})
soup = BeautifulSoup(r.content, self.required_features)
maindiv = soup.find('div', 'left')
results = []
for elem in maindiv.find_all('li'):
sid = int(match('tvshow-([0-9]+)\.html', elem.a['href']))
show_name = match('(.*) \(', elem.a.text)
results.append((show_name, sid))
#TODO: pick up the best one in a smart way
result = results[0]
return result[1]
@cachedmethod
def get_episode_id(self, series_id, season, number):
"""Get the TvSubtitles id for the given episode. Raises KeyError if none
could be found."""
# download the page of the season, contains ids for all episodes
episode_id = None
r = self.session.get('%s/tvshow-%d-%d.html' % (self.server_url, series_id, season))
soup = BeautifulSoup(r.content, self.required_features)
table = soup.find('table', id='table5')
for row in table.find_all('tr'):
cells = row.find_all('td')
if not cells:
continue
episode_number = match('x([0-9]+)', cells[0].text)
if not episode_number:
continue
episode_number = int(episode_number)
episode_id = int(match('episode-([0-9]+)', cells[1].a['href']))
# we could just return the id of the queried episode, but as we
# already downloaded the whole page we might as well fill in the
# information for all the episodes of the season
self.cache_for(self.get_episode_id, args=(series_id, season, episode_number), result=episode_id)
# raises KeyError if not found
return self.cached_value(self.get_episode_id, args=(series_id, season, number))
# Do not cache this method in order to always check for the most recent
# subtitles
def get_sub_ids(self, episode_id):
subids = []
r = self.session.get('%s/episode-%d.html' % (self.server_url, episode_id))
epsoup = BeautifulSoup(r.content, self.required_features)
for subdiv in epsoup.find_all('a'):
if 'href' not in subdiv.attrs or not subdiv['href'].startswith('/subtitle'):
continue
subid = int(match('([0-9]+)', subdiv['href']))
lang = self.get_language(match('flags/(.*).gif', subdiv.img['src']))
result = {'subid': subid, 'language': lang}
for p in subdiv.find_all('p'):
if 'alt' in p.attrs and p['alt'] == 'rip':
result['rip'] = p.text.strip()
if 'alt' in p.attrs and p['alt'] == 'release':
result['release'] = p.text.strip()
subids.append(result)
return subids
def list_checked(self, video, languages):
return self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode)
def query(self, filepath, languages, keywords, series, season, episode):
logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
self.init_cache()
sid = self.get_likely_series_id(series.lower())
try:
ep_id = self.get_episode_id(sid, season, episode)
except KeyError:
logger.debug(u'Could not find episode id for %s season %d episode %d' % (series, season, episode))
return []
subids = self.get_sub_ids(ep_id)
# filter the subtitles with our queried languages
subtitles = []
for subid in subids:
language = subid['language']
if language not in languages:
continue
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(),
'%s/download-%d.html' % (self.server_url, subid['subid']),
keywords=[subid['rip'], subid['release']])
subtitles.append(subtitle)
return subtitles
def download(self, subtitle):
self.download_zip_file(subtitle.link, subtitle.path)
Service = TvSubtitles
+21 -13
View File
@@ -15,13 +15,12 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from .languages import list_languages, convert_language
from .language import Language
import os.path
__all__ = ['Subtitle', 'EmbeddedSubtitle', 'ExternalSubtitle', 'ResultSubtitle', 'get_subtitle_path']
#: Subtitles extensions
EXTENSIONS = ['.srt', '.sub', '.txt']
@@ -30,7 +29,8 @@ class Subtitle(object):
"""Base class for subtitles
:param string path: path to the subtitle
:param string language: language of the subtitle (second element of :class:`~subliminal.languages.LANGUAGES`)
:param language: language of the subtitle
:type language: :class:`~subliminal.language.Language`
"""
def __init__(self, path, language):
@@ -49,7 +49,8 @@ class EmbeddedSubtitle(Subtitle):
"""Subtitle embedded in a container
:param string path: path to the subtitle
:param string language: language of the subtitle (second element of :class:`~subliminal.languages.LANGUAGES`)
:param language: language of the subtitle
:type language: :class:`~subliminal.language.Language`
:param int track_id: id of the subtitle track in the container
"""
@@ -59,7 +60,7 @@ class EmbeddedSubtitle(Subtitle):
@classmethod
def from_enzyme(cls, path, subtitle):
language = convert_language(subtitle.language, 1, 2)
language = Language(subtitle.language) or None
return cls(path, language, subtitle.trackno)
@@ -76,8 +77,7 @@ class ExternalSubtitle(Subtitle):
if not extension:
raise ValueError('Not a supported subtitle extension')
language = os.path.splitext(path[:len(path) - len(extension)])[1][1:]
if not language in list_languages(1):
language = None
language = Language(language) or None
return cls(path, language)
@@ -85,7 +85,8 @@ class ResultSubtitle(ExternalSubtitle):
"""Subtitle found using :mod:`~subliminal.services`
:param string path: path to the subtitle
:param string language: language of the subtitle (second element of :class:`~subliminal.languages.LANGUAGES`)
:param language: language of the subtitle
:type language: :class:`~subliminal.language.Language`
:param string service: name of the service
:param string link: download link for the subtitle
:param string release: release name of the video
@@ -111,20 +112,27 @@ class ResultSubtitle(ExternalSubtitle):
"""
extension = os.path.splitext(self.path)[0]
language = os.path.splitext(self.path[:len(self.path) - len(extension)])[1][1:]
if not language in list_languages(1):
return True
return False
return Language(language) == Language('und')
def __repr__(self):
return 'ResultSubtitle(%s, %s, %.2f, %s)' % (self.language, self.service, self.confidence, self.release)
def get_subtitle_path(video_path, language, multi):
"""Create the subtitle path from the given video path using language if multi"""
"""Create the subtitle path from the given video path using language if multi
:param string video_path: path to the video
:param language: language of the subtitle
:type language: :class:`~subliminal.language.Language`
:param bool multi: whether to use multi language naming or not
:return: path of the subtitle
:rtype: string
"""
if not os.path.exists(video_path):
path = os.path.splitext(os.path.basename(video_path))[0]
else:
path = os.path.splitext(video_path)[0]
if multi and language:
return path + '.%s%s' % (language, EXTENSIONS[0])
return path + '.%s%s' % (language.alpha2, EXTENSIONS[0])
return path + '%s' % EXTENSIONS[0]
+2
View File
@@ -35,6 +35,7 @@ class ListTask(Task):
"""
def __init__(self, video, languages, service, config):
super(ListTask, self).__init__()
self.video = video
self.service = service
self.languages = languages
@@ -54,6 +55,7 @@ class DownloadTask(Task):
"""
def __init__(self, video, subtitles):
super(DownloadTask, self).__init__()
self.video = video
self.subtitles = subtitles
+24 -11
View File
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import subtitles
from .languages import list_languages
import enzyme
import guessit
import hashlib
@@ -130,19 +129,30 @@ class Video(object):
logger.debug(u'Failed parsing %s with enzyme' % self.path)
if isinstance(video_infos, enzyme.core.AVContainer):
results.extend([subtitles.EmbeddedSubtitle.from_enzyme(self.path, s) for s in video_infos.subtitles])
for l in list_languages(1):
for e in subtitles.EXTENSIONS:
single_path = basepath + '%s' % e
if os.path.exists(single_path):
results.append(subtitles.ExternalSubtitle(single_path, None))
multi_path = basepath + '.%s%s' % (l, e)
if os.path.exists(multi_path):
results.append(subtitles.ExternalSubtitle(multi_path, l))
# cannot use glob here because it chokes if there are any square
# brackets inside the filename, so we have to use basic string
# startswith/endswith comparisons
folder, basename = os.path.split(basepath)
existing = [f for f in os.listdir(folder) if f.startswith(basename)]
for path in existing:
for ext in subtitles.EXTENSIONS:
if path.endswith(ext):
possible_lang = path[len(basename) + 1:-len(ext)]
if possible_lang == '':
results.append(subtitles.ExternalSubtitle(path, None))
else:
lang = guessit.Language(possible_lang)
if lang:
results.append(subtitles.ExternalSubtitle(path, lang))
return results
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.release)
def __hash__(self):
return hash(self.path or self.release)
class Episode(Video):
"""Episode :class:`Video`
@@ -189,11 +199,12 @@ class UnknownVideo(Video):
pass
def scan(entry, max_depth=3, depth=0):
def scan(entry, max_depth=3, scan_filter=None, depth=0):
"""Scan a path for videos and subtitles
:param string entry: path
:param int max_depth: maximum folder depth
:param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
:param int depth: starting depth
:return: found videos and subtitles
:rtype: list of (:class:`Video`, [:class:`~subliminal.subtitles.Subtitle`])
@@ -207,13 +218,15 @@ def scan(entry, max_depth=3, depth=0):
logger.debug(u'Scanning directory %s with depth %d/%d' % (entry, depth, max_depth))
result = []
for e in os.listdir(entry):
result.extend(scan(os.path.join(entry, e), max_depth, depth + 1))
result.extend(scan(os.path.join(entry, e), max_depth, scan_filter, depth + 1))
return result
if os.path.isfile(entry) or depth == 0:
logger.debug(u'Scanning file %s with depth %d/%d' % (entry, depth, max_depth))
if depth != 0: # trust the user: only check for valid format if recursing
if mimetypes.guess_type(entry)[0] not in MIMETYPES and os.path.splitext(entry)[1] not in EXTENSIONS:
return []
if scan_filter is not None and scan_filter(entry):
return []
video = Video.from_path(entry)
return [(video, video.scan())]
logger.warning(u'Scanning entry %s failed with depth %d/%d' % (entry, depth, max_depth))
+26
View File
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from . import test_language, test_services, test_subliminal
import unittest
suite = unittest.TestSuite([test_language.suite(), test_services.suite(), test_subliminal.suite()])
if __name__ == '__main__':
unittest.TextTestRunner().run(suite)
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from subliminal.language import Language, Country, language_set, language_list
import unittest
class LanguageListTestCase(unittest.TestCase):
def test_list_contains(self):
languages = list([Language('fr'), Language('en-US'), Language('en-GB')])
self.assertTrue(Language('fr') in languages)
self.assertTrue(Language('en-US') in languages)
self.assertTrue(Language('en') not in languages)
self.assertTrue(Language('fr-BE') not in languages)
def test_language_list_contains(self):
languages = language_list(['fr', 'en-US', 'en-GB'])
self.assertTrue(Language('fr') in languages)
self.assertTrue(Language('en-US') in languages)
self.assertTrue(Language('en') not in languages)
self.assertTrue(Language('fr-BE') in languages)
def test_list_index(self):
languages = [Language('fr'), Language('en-US'), Language('en-GB')]
self.assertTrue(languages.index(Language('fr')) == 0)
self.assertTrue(languages.index(Language('en-US')) == 1)
self.assertTrue(languages.index(Language('en-GB')) == 2)
with self.assertRaises(ValueError):
languages.index(Language('fr-BE'))
def test_language_list_index(self):
languages = language_list(['fr', 'en-US', 'en-GB'])
self.assertTrue(languages.index(Language('fr')) == 0)
self.assertTrue(languages.index(Language('en-US')) == 1)
self.assertTrue(languages.index(Language('en-GB')) == 2)
self.assertTrue(languages.index(Language('fr-BE')) == 0)
class LanguageSetTestCase(unittest.TestCase):
def test_set_contains(self):
languages = set([Language('fr'), Language('en-US'), Language('en-GB')])
self.assertTrue(Language('fr') in languages)
self.assertTrue(Language('en-US') in languages)
self.assertTrue(Language('en') not in languages)
self.assertTrue(Language('fr-BE') not in languages)
def test_language_set_contains(self):
languages = language_set(['fr', 'en-US', 'en-GB'])
self.assertTrue(Language('fr') in languages)
self.assertTrue(Language('en-US') in languages)
self.assertTrue(Language('en') not in languages)
self.assertTrue(Language('fr-BE') in languages)
def test_language_set_intersect(self):
languages = language_set(['fr', 'en-US', 'en-GB'])
self.assertTrue(len(languages & language_set([Language('en')])) == 2)
self.assertTrue(len(language_set([Language('en')]) & languages) == 2)
self.assertTrue(len(languages & language_set([Language('fr')])) == 1)
def test_language_set_substract(self):
languages = language_set(['fr', 'en-US', 'en-GB'])
self.assertTrue(len(languages - language_set(['en'])) == 1)
self.assertTrue(len(languages - language_set(['en-US'])) == 2)
self.assertTrue(len(languages - language_set(['en-US', 'fr'])) == 1)
class LanguageTestCase(unittest.TestCase):
def test_attrs(self):
language = Language('French')
self.assertTrue(language.alpha2 == 'fr')
self.assertTrue(language.alpha3 == 'fre')
self.assertTrue(language.terminologic == 'fra')
self.assertTrue(language.name == 'French')
self.assertTrue(language.french_name == u'français')
def test_eq(self):
language = Language('French')
self.assertTrue(language == Language('fr'))
self.assertTrue(language == Language('fre'))
self.assertTrue(language == Language('fra'))
self.assertTrue(language == Language('Français'))
def test_ne(self):
self.assertTrue(Language('French') != Language('en'))
def test_in(self):
self.assertTrue(Language('Portuguese (BR)') in Language('Portuguese - Brazil'))
self.assertTrue(Language('Portuguese (BR)') in Language('Portuguese'))
self.assertTrue(Language('Portuguese') not in Language('Portuguese (BR)'))
def test_with_country(self):
self.assertTrue(Language('Portuguese (BR)').country == Country('Brazil'))
self.assertTrue(Language('pt_BR').country == Country('Brazil'))
self.assertTrue(Language('fr - France').country == Country('France'))
self.assertTrue(Language('fra', country='FR').country == Country('France'))
self.assertTrue(Language('fra', country=Country('FRA')).country == Country('France'))
def test_eq_with_country(self):
self.assertTrue(Language('Portuguese (BR)') == Language('Portuguese - Brazil'))
self.assertTrue(Language('English') == Language('en'))
def test_ne_with_country(self):
self.assertTrue(Language('Portuguese') != Language('Portuguese (BR)'))
self.assertTrue(Language('English (US)') != Language('English (GB)'))
def test_hash(self):
self.assertTrue(hash(Language('French')) == hash('fre'))
def test_missing(self):
with self.assertRaises(ValueError):
Language('zzz')
class CountryTestCase(unittest.TestCase):
def test_attrs(self):
country = Country('France')
self.assertTrue(country.alpha2 == 'FR')
self.assertTrue(country.alpha3 == 'FRA')
self.assertTrue(country.name == 'France')
def test_eq(self):
country = Country('France')
self.assertTrue(country == Country('FR'))
self.assertTrue(country == Country('FRA'))
self.assertTrue(country == Country('250'))
def test_ne(self):
self.assertTrue(Country('France') != Country('GB'))
def test_hash(self):
self.assertTrue(hash(Country('France')) == hash('FRA'))
def test_missing(self):
with self.assertRaises(ValueError):
Country('ZZ')
def suite():
suite = unittest.TestSuite()
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(CountryTestCase))
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(LanguageTestCase))
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(LanguageSetTestCase))
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(LanguageListTestCase))
return suite
if __name__ == '__main__':
unittest.TextTestRunner().run(suite())
+332 -221
View File
@@ -17,16 +17,24 @@
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from subliminal import videos
from subliminal.exceptions import MissingLanguageError, ServiceError
from subliminal.exceptions import ServiceError
from subliminal.language import language_set, LANGUAGES
from subliminal.services import ServiceConfig
from subliminal.services.addic7ed import Addic7ed
from subliminal.services.bierdopje import BierDopje
from subliminal.services.opensubtitles import OpenSubtitles
from subliminal.services.podnapisi import Podnapisi
from subliminal.services.subswiki import SubsWiki
from subliminal.services.subtitulos import Subtitulos
from subliminal.services.thesubdb import TheSubDB
from subliminal.subtitles import Subtitle
from subliminal.services.tvsubtitles import TvSubtitles
import os
import sys
import unittest
try:
import cPickle as pickle
except ImportError:
import pickle
cache_dir = u'/tmp/sublicache'
@@ -35,20 +43,145 @@ if not os.path.exists(cache_dir):
existing_video = u'/something/The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4'
class BierDopjeTestCase(unittest.TestCase):
query_tests = ['test_query_series', 'test_query_wrong_series', 'test_query_wrong_languages',
'test_query_tvdbid', 'test_query_wrong_tvdbid', 'test_query_series_and_tvdbid']
list_tests = ['test_list_episode', 'test_list_movie', 'test_list_wrong_languages']
download_tests = ['test_download']
class ServiceTestCase(unittest.TestCase):
def setUp(self):
self.wrong_languages = language_set(list(language_set(LANGUAGES) - self.service.languages)[:2])
def tearDown(self):
# Setting config to None allows to delete the object, which will in turn save the cache
self.config = None
@unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(service, self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(service, self.fake_file, self.languages, self.episode_keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(service, self.fake_file, self.wrong_languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_list_episode(self):
video = videos.Video.from_path(self.episode_path)
with self.service(self.config) as service:
results = service.list(video, self.languages)
self.assertTrue(len(results) > 0)
def test_list_movie(self):
video = videos.Video.from_path(self.movie_path)
with self.service(self.config) as service:
results = service.list(video, self.languages)
self.assertTrue(len(results) > 0)
def test_list_wrong_languages(self):
video = videos.Video.from_path(self.episode_path)
with self.service(self.config) as service:
results = service.list(video, self.wrong_languages)
self.assertTrue(len(results) == 0)
def test_download_episode(self):
video = videos.Video.from_path(self.episode_path)
with self.service(self.config) as service:
subtitle = service.list(video, language_set([self.episode_sublanguage]))[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
service.download(subtitle)
self.assertTrue(os.path.exists(subtitle.path))
self.assertTrue(os.path.getsize(subtitle.path) in self.episode_subfilesizes, msg='Size %d not in %r' % (os.path.getsize(subtitle.path), self.episode_subfilesizes))
os.remove(subtitle.path)
def test_download_movie(self):
video = videos.Video.from_path(self.movie_path)
with self.service(self.config) as service:
subtitle = service.list(video, language_set([self.movie_sublanguage]))[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
service.download(subtitle)
self.assertTrue(os.path.exists(subtitle.path))
self.assertTrue(os.path.getsize(subtitle.path) in self.movie_subfilesizes, msg='Size %d not in %r' % (os.path.getsize(subtitle.path), self.movie_subfilesizes))
os.remove(subtitle.path)
def test_cached_series(self):
with self.service(self.config) as service:
service.clear_cache()
service.query(self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
service.save_cache()
c = pickle.load(open(os.path.join(cache_dir, 'subliminal_%s.cache' % self.service.__name__)))
found = False
for _, cached_values in c.items():
for args, __ in cached_values.items():
if args == (self.series.lower(),):
found = True
self.assertTrue(found)
class Addic7edTestCase(ServiceTestCase):
query_tests = ['test_query_series', 'test_query_wrong_series', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = ['test_cached_series']
service = Addic7ed
def setUp(self):
super(Addic7edTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.fake_file = u'/tmp/fake_file'
self.languages = language_set(['en', 'fr'])
self.episode_path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.episode_sublanguage = 'en'
# FIXME: this is the size of the first subtitle that appears on the page
# which is the original one, not the most updated one. We should make
# sure the Addic7ed service picks up the most recent one instead
self.episode_subfilesizes = [33538, 33643]
self.episode_keywords = set(['asap', 'hdtv'])
self.series = 'The Big Bang Theory'
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
class BierDopjeTestCase(ServiceTestCase):
query_tests = ['test_query_series', 'test_query_wrong_series', 'test_query_wrong_languages',
'test_query_tvdbid', 'test_query_wrong_tvdbid', 'test_query_series_and_tvdbid']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = ['test_cached_series']
service = BierDopje
def setUp(self):
super(BierDopjeTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.episode_path = u'The Big Bang Theory/Season 05/S05E06 - The Rhinitis Revelation - HD TV.mkv'
self.episode_sublanguage = 'en'
self.episode_subfilesizes = [33469]
self.movie_path = u'Inception (2010)/Inception - 1080p.mkv'
self.languages = set(['en', 'nl'])
self.wrong_languages = set(['zz', 'es'])
self.movie_sublanguage = 'en'
self.movie_subfilesizes = []
self.languages = language_set(['en', 'nl'])
self.fake_file = u'/tmp/fake_file'
self.series = 'The Big Bang Theory'
self.episode_keywords = set()
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
@@ -56,202 +189,159 @@ class BierDopjeTestCase(unittest.TestCase):
self.wrong_tvdbid = 9999999999
def test_query_series(self):
with BierDopje(self.config) as service:
results = service.query(self.season, self.episode, self.languages, self.fake_file, series=self.series)
with self.service(self.config) as service:
results = service.query(self.fake_file, self.season, self.episode, self.languages, series=self.series)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with BierDopje(self.config) as service:
results = service.query(self.season, self.episode, self.languages, self.fake_file, series=self.wrong_series)
with self.service(self.config) as service:
results = service.query(self.fake_file, self.season, self.episode, self.languages, series=self.wrong_series)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with BierDopje(self.config) as service:
results = service.query(self.season, self.episode, self.wrong_languages, self.fake_file, series=self.series)
with self.service(self.config) as service:
results = service.query(self.fake_file, self.season, self.episode, self.wrong_languages, series=self.series)
self.assertTrue(len(results) == 0)
def test_query_tvdbid(self):
with BierDopje(self.config) as service:
results = service.query(self.season, self.episode, self.languages, self.fake_file, tvdbid=self.tvdbid)
results = service.query(self.fake_file, self.season, self.episode, self.languages, tvdbid=self.tvdbid)
self.assertTrue(len(results) > 0)
def test_query_series_and_tvdbid(self):
with BierDopje(self.config) as service:
results = service.query(self.season, self.episode, self.languages, self.fake_file, series=self.series, tvdbid=self.tvdbid)
results = service.query(self.fake_file, self.season, self.episode, self.languages, series=self.series, tvdbid=self.tvdbid)
self.assertTrue(len(results) > 0)
def test_query_wrong_tvdbid(self):
with BierDopje(self.config) as service:
results = service.query(self.season, self.episode, self.languages, self.fake_file, tvdbid=self.wrong_tvdbid)
results = service.query(self.fake_file, self.season, self.episode, self.languages, tvdbid=self.wrong_tvdbid)
self.assertTrue(len(results) == 0)
def test_list_episode(self):
episode = videos.Video.from_path(self.episode_path)
with BierDopje(self.config) as service:
results = service.list(episode, self.languages)
self.assertTrue(len(results) > 0)
def test_list_movie(self):
movie = videos.Video.from_path(self.movie_path)
with BierDopje(self.config) as service:
results = service.list(movie, self.languages)
self.assertTrue(len(results) == 0)
def test_list_wrong_languages(self):
episode = videos.Video.from_path(self.episode_path)
with BierDopje(self.config) as service:
results = service.list(episode, self.wrong_languages)
self.assertTrue(len(results) == 0)
def test_download(self):
episode = videos.Video.from_path(self.episode_path)
with BierDopje(self.config) as service:
subtitle = service.list(episode, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
service.download(subtitle)
self.assertTrue(os.path.exists(subtitle.path))
def test_cached_series(self):
with self.service(self.config) as service:
service.clear_cache()
service.query(self.fake_file, self.season, self.episode, self.languages, series=self.series)
service.save_cache()
c = pickle.load(open(os.path.join(cache_dir, 'subliminal_%s.cache' % self.service.__name__)))
found = False
for _, cached_values in c.items():
for args, __ in cached_values.items():
if args == (self.series.lower(),):
found = True
self.assertTrue(found)
class OpenSubtitlesTestCase(unittest.TestCase):
class OpenSubtitlesTestCase(ServiceTestCase):
query_tests = ['test_query_query', 'test_query_imdbid', 'test_query_hash', 'test_query_wrong_languages']
list_tests = ['test_list', 'test_list_wrong_languages']
download_tests = ['test_download']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = []
service = OpenSubtitles
def setUp(self):
super(OpenSubtitlesTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.languages = set(['en', 'fr'])
self.wrong_languages = set(['zz', 'yy'])
self.languages = language_set(['en', 'fr'])
self.fake_file = u'/tmp/fake_file'
self.path = existing_video
self.series = 'The Big Bang Theory'
self.episode_path = existing_video
self.episode_sublanguage = 'en'
self.episode_subfilesizes = [33585, 33547, 33563, 33601]
self.movie = 'Inception'
self.wrong_series = 'No Existent Show Name'
self.imdbid = 'tt1375666'
self.wrong_imdbid = 'tt9999999'
self.imdbid = '1375666'
self.wrong_imdbid = '9999999'
self.hash = '51e57c4e8fd77990'
self.size = 882571264L
def test_query_query(self):
with OpenSubtitles(self.config) as service:
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, query=self.movie)
self.assertTrue(len(results) > 0)
def test_query_imdbid(self):
with OpenSubtitles(self.config) as service:
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, imdbid=self.imdbid)
self.assertTrue(len(results) > 0)
def test_query_hash(self):
with OpenSubtitles(self.config) as service:
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, moviehash=self.hash, size=self.size)
self.assertTrue(len(results) > 0)
def test_query_wrong_languages(self):
with OpenSubtitles(self.config) as service:
with self.assertRaises(MissingLanguageError):
service.query(self.fake_file, self.wrong_languages, moviehash=self.hash, size=self.size)
def test_list(self):
video = videos.Video.from_path(self.path)
with OpenSubtitles(self.config) as service:
results = service.list(video, self.languages)
self.assertTrue(len(results) > 0)
def test_list_wrong_languages(self):
video = videos.Video.from_path(self.path)
with OpenSubtitles(self.config) as service:
results = service.list(video, self.wrong_languages)
with self.service(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, moviehash=self.hash, size=self.size)
self.assertTrue(len(results) == 0)
def test_download(self):
video = videos.Video.from_path(self.path)
with OpenSubtitles(self.config) as service:
subtitle = service.list(video, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
result = service.download(subtitle)
self.assertTrue(isinstance(result, Subtitle))
self.assertTrue(os.path.exists(subtitle.path))
class TheSubDBTestCase(unittest.TestCase):
query_tests = ['test_query', 'test_query_wrong_hash', 'test_query_wrong_languages']
list_tests = ['test_list', 'test_list_wrong_languages']
download_tests = ['test_download']
class PodnapisiTestCase(ServiceTestCase):
query_tests = ['test_query', 'test_query_wrong_languages']
list_tests = [] #'test_list', 'test_list_wrong_languages'
download_tests = [] #'test_download'
cache_tests = []
service = Podnapisi
def setUp(self):
super(PodnapisiTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.path = existing_video
self.hash = u'edc1981d6459c6111fe36205b4aff6c2'
self.wrong_hash = u'ffffffffffffffffffffffffffffffff'
self.languages = set(['en', 'nl'])
self.wrong_languages = set(['zz', 'cs'])
self.languages = language_set(['en', 'fr'])
self.fake_file = u'/tmp/fake_file'
self.path = existing_video
self.hash = 'e1b45885346cfa0b'
def test_query(self):
with TheSubDB(self.config) as service:
results = service.query(self.fake_file, self.hash, self.languages)
with Podnapisi(self.config) as service:
results = service.query(self.fake_file, self.languages, moviehash=self.hash)
self.assertTrue(len(results) > 0)
def test_query_wrong_hash(self):
with TheSubDB(self.config) as service:
results = service.query(self.fake_file, self.wrong_hash, self.languages)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with TheSubDB(self.config) as service:
results = service.query(self.fake_file, self.hash, self.wrong_languages)
with Podnapisi(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, moviehash=self.hash)
self.assertTrue(len(results) == 0)
def test_list(self):
video = videos.Video.from_path(self.path)
with TheSubDB(self.config) as service:
results = service.list(video, self.languages)
self.assertTrue(len(results) > 0)
def test_list_wrong_languages(self):
video = videos.Video.from_path(self.path)
with TheSubDB(self.config) as service:
results = service.list(video, self.wrong_languages)
self.assertTrue(len(results) == 0)
def test_download(self):
video = videos.Video.from_path(self.path)
with TheSubDB(self.config) as service:
subtitle = service.list(video, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
service.download(subtitle)
self.assertTrue(os.path.exists(subtitle.path))
class SubsWikiTestCase(unittest.TestCase):
class SubsWikiTestCase(ServiceTestCase):
query_tests = ['test_query_series', 'test_query_movie', 'test_query_wrong_parameters', 'test_query_wrong_series', 'test_query_wrong_languages']
list_tests = ['test_list_series', 'test_list_movie', 'test_list_series_wrong_languages']
download_tests = ['test_download']
list_tests = ['test_list_episode', 'test_list_movie', 'test_list_wrong_languages']
download_tests = ['test_download_episode', 'test_download_movie']
cache_tests = []
service = SubsWiki
def setUp(self):
super(SubsWikiTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.fake_file = u'/tmp/fake_file'
self.languages = set(['en', 'es'])
self.wrong_languages = set(['zz', 'ay'])
self.languages = language_set(['en', 'es'])
self.movie_path = u'Soul Surfer (2011)/Soul.Surfer.(2011).DVDRip.XviD-TWiZTED.mkv'
self.movie_sublanguage = 'es'
self.movie_subfilesizes = [87528]
self.movie_keywords = set(['twizted'])
self.movie = u'Soul Surfer'
self.movie_year = 2011
self.series_path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.series_keywords = set(['asap', 'hdtv'])
self.episode_path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.episode_sublanguage = 'es'
self.episode_subfilesizes = [34040]
self.episode_keywords = set(['asap', 'hdtv'])
self.series = 'The Big Bang Theory'
self.wrong_series = 'No Existent Show Name'
self.series_season = 5
self.series_episode = 6
self.season = 5
self.episode = 6
def test_query_series(self):
with SubsWiki(self.config) as service:
results = service.query(self.fake_file, self.languages, keywords=self.series_keywords, series=self.series, season=self.series_season, episode=self.series_episode)
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_movie(self):
with SubsWiki(self.config) as service:
results = service.query(self.fake_file, self.languages, keywords=self.movie_keywords, movie=self.movie, year=self.movie_year)
@@ -259,134 +349,155 @@ class SubsWikiTestCase(unittest.TestCase):
def test_query_wrong_parameters(self):
with SubsWiki(self.config) as service:
with self.assertRaises(ServiceError):
service.query(self.fake_file, self.languages, keywords=self.movie_keywords, movie=self.movie, series=self.series)
def test_query_wrong_series(self):
with SubsWiki(self.config) as service:
results = service.query(self.fake_file, self.languages, keywords=self.series_keywords, series=self.wrong_series, season=self.series_season, episode=self.series_episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with SubsWiki(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, keywords=self.series_keywords, series=self.series, season=self.series_season, episode=self.series_episode)
self.assertTrue(len(results) == 0)
def test_list_series(self):
video = videos.Video.from_path(self.series_path)
with SubsWiki(self.config) as service:
results = service.list(video, self.languages)
self.assertTrue(len(results) > 0)
def test_list_movie(self):
video = videos.Video.from_path(self.movie_path)
with SubsWiki(self.config) as service:
results = service.list(video, self.languages)
self.assertTrue(len(results) > 0)
def test_list_series_wrong_languages(self):
video = videos.Video.from_path(self.series_path)
with SubsWiki(self.config) as service:
results = service.list(video, self.wrong_languages)
self.assertTrue(len(results) == 0)
def test_download(self):
video = videos.Video.from_path(self.series_path)
with SubsWiki(self.config) as service:
subtitle = service.list(video, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
service.download(subtitle)
self.assertTrue(os.path.exists(subtitle.path))
self.assertRaises(ServiceError, service.query,
self.fake_file, self.languages, keywords=self.movie_keywords, movie=self.movie, series=self.series)
class SubtitulosTestCase(unittest.TestCase):
query_tests = ['test_query', 'test_query_wrong_series', 'test_query_wrong_languages']
list_tests = ['test_list', 'test_list_wrong_languages']
download_tests = ['test_download']
class SubtitulosTestCase(ServiceTestCase):
query_tests = ['test_query_series', 'test_query_wrong_series', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = []
service = Subtitulos
def setUp(self):
super(SubtitulosTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.fake_file = u'/tmp/fake_file'
self.languages = set(['en', 'es'])
self.wrong_languages = set(['zz', 'ay'])
self.path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.keywords = set(['asap', 'hdtv'])
self.languages = language_set(['en', 'es'])
self.episode_path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.episode_sublanguage = 'en'
self.episode_subfilesizes = [32986]
self.episode_keywords = set(['asap', 'hdtv'])
self.series = 'The Big Bang Theory'
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
def test_query(self):
with Subtitulos(self.config) as service:
results = service.query(self.fake_file, self.languages, self.keywords, self.series, self.season, self.episode)
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with Subtitulos(self.config) as service:
results = service.query(self.fake_file, self.languages, self.keywords, self.wrong_series, self.season, self.episode)
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with Subtitulos(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, self.keywords, self.series, self.season, self.episode)
with self.service(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_list(self):
video = videos.Video.from_path(self.path)
with Subtitulos(self.config) as service:
results = service.list(video, self.languages)
class TheSubDBTestCase(ServiceTestCase):
query_tests = ['test_query', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = []
service = TheSubDB
def setUp(self):
super(TheSubDBTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.episode_path = existing_video
self.episode_sublanguage = 'en'
self.episode_subfilesizes = [33536]
self.hash = u'edc1981d6459c6111fe36205b4aff6c2'
self.languages = language_set(['en', 'nl'])
self.fake_file = u'/tmp/fake_file'
def test_query(self):
with TheSubDB(self.config) as service:
results = service.query(self.fake_file, self.hash, self.languages)
self.assertTrue(len(results) > 0)
def test_list_wrong_languages(self):
video = videos.Video.from_path(self.path)
with Subtitulos(self.config) as service:
results = service.list(video, self.wrong_languages)
def test_query_wrong_languages(self):
with TheSubDB(self.config) as service:
results = service.query(self.fake_file, self.hash, self.wrong_languages)
self.assertTrue(len(results) == 0)
def test_download(self):
video = videos.Video.from_path(self.path)
with Subtitulos(self.config) as service:
subtitle = service.list(video, self.languages)[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
result = service.download(subtitle)
self.assertTrue(os.path.exists(subtitle.path))
@unittest.skipUnless(os.path.exists(existing_video), 'No existing video')
def test_list_episode(self):
super(TheSubDBTestCase, self).test_list_episode()
@unittest.skipUnless(os.path.exists(existing_video), 'No existing video')
def test_download_episode(self):
super(TheSubDBTestCase, self).test_download_episode()
class TvSubtitlesTestCase(ServiceTestCase):
query_tests = ['test_query_series', 'test_query_wrong_series', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = ['test_cached_series']
service = TvSubtitles
def setUp(self):
super(TvSubtitlesTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.fake_file = u'/tmp/fake_file'
self.languages = language_set(['en', 'es'])
self.episode_path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.episode_sublanguage = 'en'
self.episode_subfilesizes = [33078]
self.episode_keywords = set(['asap', 'hdtv'])
self.series = 'The Big Bang Theory'
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
TESTCASES = [Addic7edTestCase, BierDopjeTestCase, OpenSubtitlesTestCase, PodnapisiTestCase, SubsWikiTestCase,
SubtitulosTestCase, TheSubDBTestCase, TvSubtitlesTestCase]
def query_suite():
suite = unittest.TestSuite()
suite.addTests(map(BierDopjeTestCase, BierDopjeTestCase.query_tests))
suite.addTests(map(OpenSubtitlesTestCase, OpenSubtitlesTestCase.query_tests))
suite.addTests(map(TheSubDBTestCase, TheSubDBTestCase.query_tests))
suite.addTests(map(SubsWikiTestCase, SubsWikiTestCase.query_tests))
suite.addTests(map(SubtitulosTestCase, SubtitulosTestCase.query_tests))
for testcase in TESTCASES:
suite.addTests(map(testcase, testcase.query_tests))
return suite
def list_suite():
suite = unittest.TestSuite()
suite.addTests(map(BierDopjeTestCase, BierDopjeTestCase.list_tests))
suite.addTests(map(OpenSubtitlesTestCase, OpenSubtitlesTestCase.list_tests))
suite.addTests(map(TheSubDBTestCase, TheSubDBTestCase.list_tests))
suite.addTests(map(SubsWikiTestCase, SubsWikiTestCase.list_tests))
suite.addTests(map(SubtitulosTestCase, SubtitulosTestCase.list_tests))
for testcase in TESTCASES:
suite.addTests(map(testcase, testcase.list_tests))
return suite
def download_suite():
suite = unittest.TestSuite()
suite.addTests(map(BierDopjeTestCase, BierDopjeTestCase.download_tests))
suite.addTests(map(OpenSubtitlesTestCase, OpenSubtitlesTestCase.download_tests))
suite.addTests(map(TheSubDBTestCase, TheSubDBTestCase.download_tests))
suite.addTests(map(SubsWikiTestCase, SubsWikiTestCase.download_tests))
suite.addTests(map(SubtitulosTestCase, SubtitulosTestCase.download_tests))
for testcase in TESTCASES:
suite.addTests(map(testcase, testcase.download_tests))
return suite
def cache_suite():
suite = unittest.TestSuite()
for testcase in TESTCASES:
suite.addTests(map(testcase, testcase.cache_tests))
return suite
def suite():
return unittest.TestSuite([query_suite(), list_suite(), download_suite(), cache_suite()])
if __name__ == '__main__':
suites = []
suites.append(query_suite())
suites.append(list_suite())
suites.append(download_suite())
unittest.TextTestRunner(verbosity=2).run(unittest.TestSuite(suites))
unittest.TextTestRunner(verbosity=2).run(suite())
@@ -16,7 +16,7 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from subliminal import Pool
from subliminal import Pool, list_subtitles, download_subtitles
import os
import time
import unittest
@@ -28,6 +28,32 @@ if not os.path.exists(cache_dir):
existing_video = u'/something/The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4'
class ApiTestCase(unittest.TestCase):
def test_list_subtitles(self):
results = list_subtitles(existing_video, languages=['en', 'fr'], cache_dir=cache_dir, max_depth=3)
self.assertTrue(len(results) > 0)
def test_download_subtitles(self):
results = download_subtitles(existing_video, languages=['en', 'fr'], cache_dir=cache_dir, max_depth=3)
self.assertTrue(len(results) == 1)
for video, subtitles in results.iteritems():
self.assertTrue(video.release == existing_video)
self.assertTrue(len(subtitles) == 1)
for subtitle in subtitles:
self.assertTrue(os.path.exists(subtitle.path))
os.remove(subtitle.path)
def test_download_multi_subtitles(self):
results = download_subtitles(existing_video, languages=['en', 'fr'], cache_dir=cache_dir, max_depth=3, multi=True)
self.assertTrue(len(results) == 1)
for video, subtitles in results.iteritems():
self.assertTrue(video.release == existing_video)
self.assertTrue(len(subtitles) == 2)
for subtitle in subtitles:
self.assertTrue(os.path.exists(subtitle.path))
os.remove(subtitle.path)
class AsyncTestCase(unittest.TestCase):
def test_pool(self):
p = Pool(4)
@@ -51,8 +77,21 @@ class AsyncTestCase(unittest.TestCase):
def test_download_subtitles(self):
with Pool(4) as p:
results = p.download_subtitles(existing_video, languages=['en', 'fr'], cache_dir=cache_dir, max_depth=3)
self.assertTrue(len(results) > 0)
self.assertTrue(len(results) == 1)
for video, subtitles in results.iteritems():
self.assertTrue(video.release == existing_video)
self.assertTrue(len(subtitles) == 1)
for subtitle in subtitles:
self.assertTrue(os.path.exists(subtitle.path))
os.remove(subtitle.path)
def suite():
suite = unittest.TestSuite()
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(ApiTestCase))
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AsyncTestCase))
return suite
if __name__ == '__main__':
unittest.main()
unittest.TextTestRunner().run(suite())