Compare commits

...

135 Commits

Author SHA1 Message Date
Antoine Bertin e7f89c1a19 Release 0.7.2 2013-11-10 11:06:46 +01:00
Antoine Bertin be4f9d92eb Remove unused import 2013-11-10 11:03:37 +01:00
Antoine Bertin ca63b97e79 Parse IETF language format in cli 2013-11-10 10:25:00 +01:00
Antoine Bertin c1ed4a0232 Fix exception handling when validating subtitle 2013-11-10 10:24:23 +01:00
Antoine Bertin b826a0bf08 Use debug level for subtitle track detection 2013-11-10 10:23:41 +01:00
Antoine Bertin fd0d87d719 Use info level when skipping providers 2013-11-10 10:23:24 +01:00
Antoine Bertin 094373f3c1 Add podnapisi provider 2013-11-10 10:22:33 +01:00
Antoine Bertin 5dac623c9f Update to babelfish 0.3.0 2013-11-09 20:25:39 +01:00
Antoine Bertin 57d1e772ec Use more list comprehension 2013-11-09 18:09:19 +01:00
Antoine Bertin 983efbfd9b Reduce debug logging in opensubtitles 2013-11-09 18:06:52 +01:00
Antoine Bertin 1f11e293c1 Add missing docstring 2013-11-09 18:06:17 +01:00
Antoine Bertin bfd278ae1c Change Subtitle repr language format 2013-11-09 18:05:48 +01:00
Antoine Bertin dbe1b9d2af Update guessit requirement 2013-11-09 03:04:13 +01:00
Antoine Bertin d71bc4bf09 Set CLI default cache expiration time to 30 days 2013-11-07 00:33:38 +01:00
Antoine Bertin faf2e1dfa4 Add a CACHE_VERSION to force cache reloading on version change 2013-11-07 00:26:06 +01:00
Antoine Bertin 93360aa1bb Fix find_show_id in tvsubtitles for ambiguous series 2013-11-06 21:08:09 +01:00
Antoine Bertin 8df7780ef9 Switch to 0.7.2 2013-11-06 00:42:44 +01:00
Antoine Bertin c1fda7f44c Release 0.7.1 2013-11-06 00:32:33 +01:00
Antoine Bertin ce42201eee Improve exceptions in addic7ed terminate 2013-11-06 00:31:49 +01:00
Antoine Bertin f65131e5b0 Use the full path to guess in scan_video 2013-11-06 00:20:26 +01:00
Antoine Bertin 8d9efa5dc0 Catch exceptions during provider terminate in api 2013-11-06 00:19:45 +01:00
Antoine Bertin 1a54bfb732 Fix ProviderNotAvailable in list_subtitles 2013-11-06 00:03:53 +01:00
Antoine Bertin a15c1f05b2 Skip links and hidden files and folders in scan_video 2013-11-05 23:25:57 +01:00
Antoine Bertin ccfd341fe9 Fix enzyme track detection 2013-11-05 00:23:24 +01:00
Antoine Bertin aab8e0aa4d Fix spacing around operator 2013-11-04 23:14:25 +01:00
Antoine Bertin 9b669c8a3d Update opensubtitles unittests 2013-11-04 00:56:21 +01:00
Antoine Bertin 50960fed24 Fix single subtitles being always downloaded 2013-11-04 00:43:16 +01:00
Antoine Bertin ff61bc8d2d Add colors to debug log 2013-11-04 00:34:02 +01:00
Antoine Bertin e5d9c229ed Improve CLI 2013-11-04 00:08:20 +01:00
Antoine Bertin b0e38c7e2c Remove unnecessary personal information from addic7ed logging 2013-11-04 00:08:10 +01:00
Antoine Bertin c12dade5ea Add tests for scan_video 2013-11-03 11:53:29 -05:00
Antoine Bertin cb35dabf31 Remove lxml dependency 2013-11-03 11:52:39 -05:00
Antoine Bertin a66cf4b501 Require babelfish 0.2.1 2013-11-03 11:29:42 -05:00
Antoine Bertin c355d6a24a Fix hashes computation for small files 2013-11-03 11:26:34 -05:00
Antoine Bertin 60c1e93037 Scan for und languages and fix download for single 2013-11-01 08:38:24 +01:00
Antoine Bertin 708126aca3 Fix subtitle language filtering of videos 2013-10-31 23:24:49 +01:00
Antoine Bertin bf763a3ad7 Always log found embedded subtitles 2013-10-31 23:23:36 +01:00
Antoine Bertin bb0c3b91a2 Fix the download loop not breaking when done 2013-10-31 22:49:46 +01:00
Antoine Bertin a7c0cd0d19 Fix intersection of sets 2013-10-31 22:49:01 +01:00
Antoine Bertin f315ef9bd0 Fix subtitle language detection in api 2013-10-31 22:32:43 +01:00
Antoine Bertin b262a5491c Fix embedded subtitles always being scanned 2013-10-31 20:47:28 +01:00
Antoine Bertin d7b30336b6 Fix badly encoded subtitles 2013-10-31 20:46:19 +01:00
Antoine Bertin c834bac460 Use absolute paths in cli 2013-10-31 08:37:37 +01:00
Antoine Bertin 36da0a1204 Validate cache-file in cli 2013-10-31 08:37:28 +01:00
Antoine Bertin 6ece9271eb Require absolute paths in scan_video and scan_videos 2013-10-31 08:36:13 +01:00
Antoine Bertin a465058bb3 Catch enzyme and babelfish exceptions in scan_video 2013-10-30 22:47:13 +01:00
Antoine Bertin bad7dbb55c Explicitly use utf-8 for subtitle files encoding 2013-10-30 21:07:18 +01:00
Antoine Bertin 501aaf076e Handle download limit in Addic7ed 2013-10-30 21:05:48 +01:00
Antoine Bertin 49d27cc7e4 Add login support for Addic7ed 2013-10-30 08:40:03 +01:00
Antoine Bertin e3903f77e9 Use babelfish 0.2.0 2013-10-30 08:39:06 +01:00
Antoine Bertin b6ee9b5d7e Fix required language argument in cli 2013-10-30 08:30:11 +01:00
Antoine Bertin e3301cefd7 Fix language extensions loading
The alpha2 converter might not yet be loaded at this point.
When subliminal's converters are loaded, they import subliminal
which import the video module.
2013-10-30 08:29:43 +01:00
Antoine Bertin 74ac38329e Fix None not being a valid language for embedded subtitles 2013-10-30 08:24:15 +01:00
Antoine Bertin 50c39382e6 Improve error logging 2013-10-29 18:07:26 +01:00
Antoine Bertin 9a65708855 Add missing age implementation 2013-10-29 18:06:57 +01:00
Antoine Bertin 4ebcb2cc95 Fix video_types in some providers 2013-10-29 17:26:56 +01:00
Antoine Bertin 0c3c41fb4a Fix unsupported type for timedelta in cli 2013-10-29 17:26:29 +01:00
Antoine Bertin f44942f78e Switch to 0.7.1 2013-10-29 17:23:41 +01:00
Antoine Bertin d7f2211800 Update dev requirements for sphinx documentation upload in pypi 2013-10-29 12:59:50 +01:00
Antoine Bertin f11402c452 Complete rewrite of subliminal 2013-10-29 12:22:21 +01:00
Antoine Bertin 7878fb2f92 Update unittests 2013-01-20 00:01:52 +01:00
Antoine Bertin f90c4634c2 Revamp TVsubtitles
- Use dogpile.cache
- Compute confidence based on votes
- Improved keywords detection
- Add docstrings
- Rename to TVsubtitles
- Add unittests
2013-01-19 23:26:40 +01:00
Antoine Bertin b52fbc8af5 Unset language_code in PodnapisiWeb 2013-01-19 21:42:34 +01:00
Antoine Bertin cde1175a7b Update default User-Agent 2013-01-19 21:42:06 +01:00
Antoine Bertin 30786743e3 Update TheSubDB and add unittests 2013-01-19 15:25:45 +01:00
Antoine Bertin c675bad95c Update Subtitulos and add unittests 2013-01-19 15:03:35 +01:00
Antoine Bertin 288b92d2ce Update SubsWiki and add unittests 2013-01-19 15:03:06 +01:00
Antoine Bertin f6e656117c Add unittests for PodnapisiWeb 2013-01-19 15:02:11 +01:00
Antoine Bertin 896baa6a04 Add unittests for Podnapisi 2013-01-18 20:54:37 +01:00
Antoine Bertin b8f820a9af Fix download method in Podnapisi 2013-01-18 20:52:52 +01:00
Antoine Bertin eb8b046dbd Use new is_valid_subtitle method
Thanks to byroot
2013-01-18 20:52:13 +01:00
Antoine Bertin ff9e0f3190 Fix Video path being empty even if forced in unittests 2013-01-18 20:51:27 +01:00
Antoine Bertin e4f10b9e77 Update addic7ed to use dogpile.cache 2013-01-13 21:25:13 +01:00
Antoine Bertin 5ca2623c13 Merge remote-tracking branch 'rik/requests-changes-172' into develop 2013-01-13 16:47:59 +01:00
Antoine Bertin e560823271 Update setup 2013-01-13 16:46:27 +01:00
Antoine Bertin 3f83b13a29 Use setuptools entry points 2013-01-13 16:46:20 +01:00
Antoine Bertin 507bf09466 Update version to 0.7-dev 2013-01-13 15:57:34 +01:00
Antoine Bertin 688a07dbe9 Update requirements 2013-01-13 15:55:36 +01:00
Antoine Bertin e7c4bc9fc3 Move tests under subliminal module 2013-01-13 15:53:24 +01:00
Anthony Ricaud 04c74afad6 Update to the latest requests API.
fix #172
2013-01-13 14:30:33 +01:00
Antoine Bertin e8a4521b7e Add query unittests for BierDopje and OpenSubtitles 2012-12-16 21:31:49 +01:00
Antoine Bertin 768b9d203c Update new unittests
- Do not expect the exact count of results in list tests
- Download tests
2012-12-16 10:33:32 +01:00
Antoine Bertin ba5e03342f Use key_mangler to fix the error in dbm backend in dogpile.cache 2012-12-15 13:33:25 +01:00
Antoine Bertin cb3ff0334d Add unittests for BierDopje 2012-12-15 13:32:38 +01:00
Antoine Bertin b96bf9b9a5 Finish list unittests for OpenSubtitles 2012-12-15 13:08:05 +01:00
Antoine Bertin 4bf673dfc6 Update new unittests 2012-12-11 14:55:01 +01:00
Antoine Bertin 223fe8025c Use dogpile.cache 2012-12-10 23:31:44 +01:00
Antoine Bertin cf47cf1512 Update requirements
- Add charade and pysrt as test_require
- Add dogpile.cache as install_require
2012-12-10 23:27:57 +01:00
Antoine Bertin 1bac0c3e8c New unittests for services using a yaml configuration file 2012-12-10 23:10:15 +01:00
Antoine Bertin 4634c353d8 Fix subswiki on some movies 2012-12-10 14:05:33 +01:00
Antoine Bertin 9ac2dde4e1 Shorten travis install part 2012-12-09 16:22:58 +01:00
Antoine Bertin 07a6db43e6 Fix test_videos by using unicode 2012-12-09 13:34:17 +01:00
Antoine Bertin c40f1f0cc2 Update unittests for PodnapisiWeb 2012-12-09 11:12:06 +01:00
Antoine Bertin c4ddc6d793 Fix subtitles validation
Subtitles are considered valid if their first 50 lines are
2012-12-09 11:11:48 +01:00
Antoine Bertin 8f58974a1a Remove keywords parameter in PodnapisiWeb as it is not supported 2012-12-09 10:48:07 +01:00
Antoine Bertin b17fbd6fc0 Ensure paths are unicode in get_defaults 2012-12-09 10:46:47 +01:00
Antoine Bertin 57d5d57c53 Fix subtitles validation 2012-12-09 01:14:13 +01:00
Antoine Bertin 5436512b1f Allow empty keywords in services
Applies to addic7ed, subswiki and subtitulos
2012-12-09 00:42:32 +01:00
Antoine Bertin 40e10ea72c Replace filesizes checks in unittests with subtitles validation
Use pysrt for subtitles validation
2012-12-09 00:38:34 +01:00
Antoine Bertin ea6aba4e92 Skip badly encoded paths and require unicode for api calls 2012-12-08 20:21:45 +01:00
Antoine Bertin e4817dd0d3 Update filesizes in unittests 2012-12-07 14:55:55 +01:00
Antoine Bertin 751a2d822a Fix PodnapisiWeb service
- Remove the sR parameter that does not accept multiple
  values and is case-sensitive
- Ensure the languageId is an integer
- Update filesizes in unittests
2012-12-07 14:55:43 +01:00
Antoine Bertin 38cc8422b6 Fix PodnapisiWeb and add unittests 2012-09-23 15:03:06 +02:00
Antoine Bertin 35f2e47195 Fix unittests for SubsWiki 2012-09-23 14:53:20 +02:00
Antoine Bertin ab3ad0fe70 Merge pull request #113 from abenea/podnapisi
Podnapisi
2012-09-22 06:37:13 -07:00
Antoine Bertin ccf26730a2 Fix repr of ServiceConfig when no cache_dir is given 2012-09-22 13:17:36 +02:00
Antoine Bertin c23868dfb1 Merge remote-tracking branch 'wackou/develop' into develop
- Move get_defaults to core
- Add new functions to __all__
- Fix download_subtitles in api
2012-09-22 12:43:03 +02:00
Antoine Bertin b24af17326 Add python 2.6 compatibility on download_zip_file 2012-09-22 11:53:37 +02:00
Antoine Bertin 45aff11bff Fix Subtitulos on incomplete subtitles and use bs4 syntax 2012-09-22 11:45:36 +02:00
Antoine Bertin 1ee700fa9d Merge branch 'develop' 2012-09-15 13:29:27 +02:00
Antoine Bertin 4f74dc9031 Bump version number 2012-09-15 13:28:11 +02:00
Antoine Bertin b53fd0bd61 Fix enzyme import in videos 2012-09-15 13:05:58 +02:00
Antoine Bertin 80e3514d56 Update copyright notice on Addic7ed 2012-09-15 13:05:35 +02:00
Antoine Bertin 3f1cac3ccc Add Galician and Catalan languages to Addic7ed 2012-09-15 13:05:21 +02:00
Antoine Bertin 261e4e8f67 Fix OpenSubtitles testcase 2012-09-15 11:58:20 +02:00
Antoine Bertin bec7ec1901 Fix SubsWiki 2012-09-15 11:29:45 +02:00
Antoine Bertin 69015293a4 Fix OpenSubtitles testcase 2012-09-15 11:28:58 +02:00
Antoine Bertin ca55e417ee Remove unused function in Addic7ed 2012-09-15 11:28:40 +02:00
Antoine Bertin ed37415ee2 Use relative imports in Subtitulos 2012-09-12 23:59:31 +02:00
Antoine Bertin 68dc99f7ab Fix Addic7ed 2012-09-12 23:51:17 +02:00
Antoine Bertin 4d9cac8941 Fix unittests for BierDopje 2012-09-12 21:52:59 +02:00
Antoine Bertin fbd6fe00d6 Add a user agent to BierDopje as requested by the service 2012-09-12 21:52:40 +02:00
Antoine Bertin e491680dff List supported services in CLI help message 2012-09-12 21:29:11 +02:00
Antoine Bertin ffc8474918 Test current directory if no folder is given while scanning 2012-09-12 21:28:43 +02:00
Antoine Bertin dd7f26e51e Update diaoul-sphinx-themes 2012-09-12 07:42:13 +02:00
Nicolas Wack 498460031e Fixed missing import 2012-07-30 00:25:57 +02:00
Nicolas Wack d977e3569f Fixed get_defaults() function call 2012-07-30 00:25:05 +02:00
Antoine Bertin f122e7e4ed Merge pull request #114 from abenea/ass
Add the .ass subtitle extension
2012-07-15 02:13:40 -07:00
Andrei Benea 3af3db3df6 Request XML search results. 2012-07-09 21:35:46 +03:00
Andrei Benea f4246de8a7 Add the .ass subtitle extension. 2012-07-07 11:20:54 +03:00
Andrei Benea 43b647ed28 Enable the PodnapisiWeb service. 2012-07-07 11:09:18 +03:00
Andrei Benea 04958f63de PodnapisiWeb service based on the simple web interface. 2012-07-07 05:03:43 +03:00
Nicolas Wack b15b2c0e7b Factored out consume_task_list() to avoid code duplication 2012-07-01 16:39:45 +02:00
Nicolas Wack 61909b68cc Factored out get_defaults() function to avoid code duplication 2012-07-01 16:37:03 +02:00
Antoine Bertin 71c91bed29 Control subtitles naming in unittest 2012-06-26 19:49:34 +02:00
77 changed files with 3336 additions and 5704 deletions
+5
View File
@@ -0,0 +1,5 @@
[report]
exclude_lines =
def __repr__
raise NotImplementedError
if __name__ == .__main__.:
+3 -3
View File
@@ -38,6 +38,6 @@ pip-log.txt
docs/_build
# Subliminal unittests
tests/*.srt
tests/*_files
tests/*_cache
subliminal/tests/*.srt
subliminal/tests/*_files
subliminal/tests/*_cache
+18 -3
View File
@@ -1,9 +1,24 @@
language: python
python:
- "2.7"
install:
- pip install coveralls --use-mirrors
- pip install -r requirements.txt --use-mirrors
- pip install -r optional-requirements.txt --use-mirrors
script: python setup.py test
script:
- coverage run --source=subliminal setup.py test
after_success:
- coveralls
notifications:
irc: "irc.freenode.org#subliminal"
email: false
irc:
channels:
- "irc.freenode.org#subliminal"
on_success: change
on_failure: always
use_notice: true
skip_join: true
-674
View File
@@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
-165
View File
@@ -1,165 +0,0 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
+55 -2
View File
@@ -1,5 +1,58 @@
News
====
Changelog
=========
0.7.2
-----
**release date:** 2013-11-10
* Fix TVSubtitles for ambiguous series
* Add a CACHE_VERSION to force cache reloading on version change
* Set CLI default cache expiration time to 30 days
* Add podnapisi provider
* Support script for languages e.g. Latn, Cyrl
* Improve logging levels
* Fix subtitle validation in some rare cases
0.7.1
-----
**release date:** 2013-11-06
* Improve CLI
* Add login support for Addic7ed
* Remove lxml dependency
* Many fixes
0.7.0
-----
**release date:** 2013-10-29
**WARNING:** Complete rewrite of subliminal with backward incompatible changes
* Use enzyme to parse metadata of videos
* Use babelfish to handle languages
* Use dogpile.cache for caching
* Use charade to detect subtitle encoding
* Use pysrt for subtitle validation
* Use entry points for subtitle providers
* New subtitle score computation
* Hearing impaired subtitles support
* Drop async support
* Drop a few providers
* And much more...
0.6.2
-----
**release date:** 2012-09-15
* Fix BierDopje
* Fix Addic7ed
* Fix SubsWiki
* Fix missing enzyme import
* Add Catalan and Galician languages to Addic7ed
* Add possible services in help message of the CLI
* Allow existing filenames to be passed without the ./ prefix
0.6.1
-----
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 Antoine Bertin
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+1 -1
View File
@@ -1 +1 @@
include COPYING COPYING.LESSER NEWS.rst README.rst
include LICENSE HISTORY.rst requirements.txt
+34 -26
View File
@@ -1,48 +1,56 @@
Subliminal
==========
.. image:: https://secure.travis-ci.org/Diaoul/subliminal.png?branch=develop
Subliminal is a python library to search and download subtitles.
It comes with an easy to use CLI (command-line interface) suitable for direct use or cron jobs.
It uses video hashes and the powerful `guessit <http://guessit.readthedocs.org/>`_ library
that extracts informations from filenames or filepaths to ensure you have the best subtitles.
It also relies on `enzyme <https://github.com/Diaoul/enzyme>`_ to detect embedded subtitles
and avoid duplicates.
.. image:: https://travis-ci.org/Diaoul/subliminal.png?branch=develop
:target: https://travis-ci.org/Diaoul/subliminal
Features
--------
Multiple subtitles services are available:
.. image:: https://coveralls.io/repos/Diaoul/subliminal/badge.png?branch=develop
:target: https://coveralls.io/r/Diaoul/subliminal?branch=develop
Providers
---------
Subliminal uses multiple providers to give users a vast choice and have a better chance to find the
best matching subtitles. Providers are extensible through a dedicated entry point.
* Addic7ed
* BierDopje
* OpenSubtitles
* SubsWiki
* Subtitulos
* TheSubDB
* TvSubtitles
You can use main subliminal's functions with a **file path**, a **file name** or a **folder path**.
Usage
-----
CLI
^^^
Download english subtitles::
$ subliminal -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
**************************************************
Downloaded 1 subtitle(s) for 1 video(s)
The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.srt from opensubtitles
**************************************************
$ subliminal -l en -- The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
1 subtitle downloaded
Module
^^^^^^
List english subtitles::
Library
^^^^^^^
Download best subtitles in French and English for videos less than one week old in a video folder,
skipping videos that already have subtitles whether they are embedded or not::
>>> subliminal.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
from babelfish import Language
from datetime import timedelta
import subliminal
# configure the cache
subliminal.cache_region.configure('dogpile.cache.dbm', arguments={'filename': '/path/to/cachefile.dbm'})
Multi-threaded use
^^^^^^^^^^^^^^^^^^
Use 4 workers to achieve the same result::
# scan for videos in the folder and their subtitles
videos = scan_videos(['/path/to/video/folder'], subtitles=True, embedded_subtitles=True)
>>> with subliminal.Pool(4) as p:
... p.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
# download
subliminal.download_best_subtitles(videos, {Language('eng'), Language('fra')}, age=timedelta(week=1))
License
-------
MIT
+4
View File
@@ -0,0 +1,4 @@
sympy>=0.7.3
sphinx>=1.1.3
sphinxcontrib-programoutput>=0.8
Sphinx-PyPI-upload>=0.2.1
+25 -1
View File
@@ -7,6 +7,11 @@ SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
@@ -29,17 +34,20 @@ help:
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@@ -108,6 +116,12 @@ latexpdf:
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@@ -151,3 +165,13 @@ doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

+2 -2
View File
@@ -1,4 +1,4 @@
<h3>About</h3>
<h3>Subliminal</h3>
<p>
Subliminal is a python library to search and download subtitles.
Subliminal is a Python library to search and download subtitles.
</p>
-6
View File
@@ -1,6 +0,0 @@
<h3>Useful Links</h3>
<ul>
<li><a href="http://pypi.python.org/pypi/subliminal">subliminal @ PyPI</a></li>
<li><a href="http://github.com/Diaoul/subliminal">subliminal @ GitHub</a></li>
<li><a href="http://github.com/Diaoul/subliminal/issues">Issue Tracker</a></li>
</ul>
+8
View File
@@ -0,0 +1,8 @@
API
===
.. module:: subliminal.api
.. autodata:: PROVIDERS_ENTRY_POINT
.. autofunction:: list_subtitles
.. autofunction:: download_subtitles
.. autofunction:: download_best_subtitles
+11
View File
@@ -0,0 +1,11 @@
Cache
=====
.. module:: subliminal.cache
.. autodata:: CACHE_VERSION
.. autofunction:: subliminal_key_generator
.. data:: region
The dogpile.cache region
Refer to `dogpile.cache's documentation <http://dogpilecache.readthedocs.org>`_ to see how to configure the region
+7
View File
@@ -0,0 +1,7 @@
CLI
===
.. module:: subliminal.cli
subliminal
----------
.. program-output:: subliminal --help
+9
View File
@@ -0,0 +1,9 @@
Exceptions
==========
.. module:: subliminal.exceptions
.. autoclass:: Error
.. autoclass:: ProviderError
.. autoclass:: ProviderConfigurationError
.. autoclass:: ProviderNotAvailable
.. autoclass:: InvalidSubtitle
+6
View File
@@ -0,0 +1,6 @@
Providers
=========
.. module:: subliminal.providers
.. autoclass:: Provider
:members:
+6
View File
@@ -0,0 +1,6 @@
Score
=====
.. module:: subliminal.score
.. autofunction:: get_episode_equations
.. autofunction:: get_movie_equations
+9
View File
@@ -0,0 +1,9 @@
Subtitle
========
.. module:: subliminal.subtitle
.. autoclass:: Subtitle
:members:
.. autofunction:: get_subtitle_path
.. autofunction:: is_valid_subtitle
.. autofunction:: compute_guess_matches
+17
View File
@@ -0,0 +1,17 @@
Video
=====
.. module:: subliminal.video
.. autodata:: VIDEO_EXTENSIONS
.. autodata:: SUBTITLE_EXTENSIONS
.. autoclass:: Video
:members:
.. autoclass:: Episode
:members:
.. autoclass:: Movie
:members:
.. autofunction:: hash_opensubtitles
.. autofunction:: hash_thesubdb
.. autofunction:: scan_subtitle_languages
.. autofunction:: scan_video
.. autofunction:: scan_videos
+28 -14
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# subliminal documentation build configuration file, created by
# sphinx-quickstart on Tue Feb 28 16:33:06 2012.
# sphinx-quickstart on Wed Oct 23 23:24:28 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
@@ -18,7 +18,7 @@ import sys, os
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('..'))
sys.path.append(os.path.abspath('_themes'))
import subliminal.infos
import subliminal
# -- General configuration -----------------------------------------------------
@@ -27,7 +27,7 @@ import subliminal.infos
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc']
extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.programoutput']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -42,17 +42,17 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = u'subliminal'
copyright = u'2012, Antoine Bertin'
project = subliminal.__title__
copyright = ' '.join(subliminal.__copyright__.split()[1:])
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = subliminal.infos.__version__
version = subliminal.__version__
# The full version, including alpha/beta/rc tags.
release = version
release = subliminal.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -88,6 +88,9 @@ pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# -- Options for HTML output ---------------------------------------------------
@@ -100,10 +103,14 @@ html_theme = 'diaoul'
# documentation.
html_theme_options = {'github_user': 'Diaoul',
'github_repo': 'subliminal',
'github_branch': 'develop',
'github_branch': 'master',
'fork_me': 1,
'flattr_href': 'http://subliminal.readthedocs.org/',
'flattr_thing_url': 'http://flattr.com/thing/629842/Subliminal'}
'flattr': 0,
'gittip': 'Diaoul',
'pypi_downloads': 1,
'pypi_version': 0,
'travis': 0,
'coveralls': 0}
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = ['_themes']
@@ -117,7 +124,7 @@ html_theme_path = ['_themes']
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
html_logo = '_static/subliminal-logo.png'
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
@@ -139,9 +146,11 @@ html_static_path = ['_static']
# Custom sidebar templates, maps document names to template names.
html_sidebars = {
'index': ['sidebar-intro.html', 'sidebar-watch.html', 'sidebar-travis-ci.html', 'sidebar-donate.html', 'localtoc.html', 'sidebar-links.html', 'searchbox.html'],
'**': ['localtoc.html', 'relations.html', 'sourcelink.html']
}
'index': ['sidebar-intro.html', 'sidebar-star.html', 'sidebar-pypi.html', 'sidebar-donate.html',
'sourcelink.html', 'searchbox.html'],
'**': ['sidebar-intro.html', 'sidebar-star.html', 'sidebar-pypi.html', 'sidebar-donate.html',
'localtoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html']
}
# Additional templates that should be rendered to pages, maps page names to
# template names.
@@ -251,5 +260,10 @@ texinfo_documents = [
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# -- Options for autodoc -------------------------------------------------------
autodoc_member_order = 'bysource'
-98
View File
@@ -1,98 +0,0 @@
This guide is going to explain the main logic of subliminal and detail
every class or function.
Services
--------
Subliminal aims at downloading subtitles. Over the web, one can find subtitles
combining different websites but there is no guarantee of a perfect match.
Even if OpenSubtitles has a gigantic subtitles database, you may not be able to
find a subtitle on it but you will find it elsewhere, say BierDopje. Sometimes,
it just takes some time before it shows up on a website even if already available
on another, but you do not want to wait to watch the latest Big Bang Theory, right?
Given this, to be reliable, subliminal has to use different :mod:`~subliminal.services`
and use a unified method to gather them all. The :class:`~subliminal.services.ServiceBase`
class will achieve this.
.. automodule:: subliminal.services
:members:
Languages
---------
To be able to support many languages, subliminal makes heavy use of ISO-3166 and ISO-639
in a dedicated module.
.. automodule:: subliminal.language
:members:
.. data:: subliminal.language.COUNTRIES
ISO-3166-1 countries list from `Debian package iso-codes 3.36-1 <http://packages.debian.org/fr/sid/iso-codes>`_.
Each item of this list is a tuple like ``(alpha2, alpha3, numeric, name)``
.. data:: subliminal.language.LANGUAGES
ISO-639-2 languages list from `the official source <http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt>`_.
Each item of this list is a tuple like ``(alpha3, terminologic, alpha2, name, french_name)``
Tasks
-----
Subliminal is IO bound: it mostly waits for IO operations (web requests) to complete.
Thus, subliminal is a good place for multi-threading. It works with atomic operations
represented by a :class:`~subliminal.tasks.Task` class which can be consumed with
:func:`~subliminal.core.consume_task` but we'll see that later.
.. automodule:: subliminal.tasks
:members:
Asynchronous
------------
To consume those tasks in an asynchronous way without flooding services with requests,
subliminal uses multiple instances of the :class:`~subliminal.async.Worker` class that
will consume the same task queue. Each worker will only create a single instance of each
:mod:`service <subliminal.services>` and this save some initialization time.
The :class:`~subliminal.async.Pool` is here to instantiate and manage multiple workers
at a time.
.. automodule:: subliminal.async
:members:
Core
----
The goal of subliminal's :mod:`~subliminal.core` module is to merge results from
consumed tasks. Merging has to be intelligent and take user preferences into account.
Core module is thus responsible for the computation of a :func:`matching confidence
<subliminal.core.matching_confidence>` so the user knows the chances that the
:class:`~subliminal.subtitles.ResultSubtitle` matches the :class:`~subliminal.videos.Video`
.. automodule:: subliminal.core
:members:
Other objects
-------------
Subliminal uses some other self-explanatory functions and classes listed below.
Video
^^^^^
.. automodule:: subliminal.videos
:members:
Subtitle
^^^^^^^^
.. automodule:: subliminal.subtitles
:members:
Utilities
^^^^^^^^^
.. automodule:: subliminal.utils
:members:
Exceptions
^^^^^^^^^^
.. automodule:: subliminal.exceptions
:members:
+66 -43
View File
@@ -1,5 +1,5 @@
.. subliminal documentation master file, created by
sphinx-quickstart on Tue Feb 28 16:33:06 2012.
sphinx-quickstart on Wed Oct 23 23:24:28 2013.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
@@ -8,72 +8,95 @@ Subliminal
Release v\ |version|
Subliminal is a python library to search and download subtitles.
It comes with an easy to use :abbr:`CLI (command-line interface)` suitable for direct use or cron jobs.
It uses video hashes and the powerful `guessit <http://guessit.readthedocs.org/>`_ library
that extracts informations from filenames or filepaths to ensure you have the best subtitles.
It also relies on `enzyme <https://github.com/Diaoul/enzyme>`_ to detect embedded subtitles
and avoid duplicates.
Features
--------
Multiple subtitles services are available:
Providers
---------
Subliminal uses multiple providers to give users a vast choice and have a better chance to find the
best matching subtitles. Providers are extensible through a dedicated entry point.
* Addic7ed
* BierDopje
* OpenSubtitles
* SubsWiki
* Subtitulos
* TheSubDB
* TvSubtitles
You can use main subliminal's functions with a **file path**, a **file name** or a **folder path**.
Usage
-----
CLI
^^^
Download english subtitles::
$ subliminal -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
**************************************************
Downloaded 1 subtitle(s) for 1 video(s)
The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.srt from opensubtitles
**************************************************
$ subliminal -l en -- The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
1 subtitle downloaded
Module
^^^^^^
List english subtitles::
See :mod:`subliminal.cli`
>>> subliminal.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
Library
^^^^^^^
Download best subtitles in French and English for videos less than one week old in a video folder,
skipping videos that already have subtitles whether they are embedded or not::
Multi-threaded use
^^^^^^^^^^^^^^^^^^
Use 4 workers to achieve the same result::
from babelfish import Language
from datetime import timedelta
import subliminal
# configure the cache
subliminal.cache_region.configure('dogpile.cache.dbm', arguments={'filename': '/path/to/cachefile.dbm'})
>>> with subliminal.Pool(4) as p:
... p.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
# scan for videos in the folder and their subtitles
videos = scan_videos(['/path/to/video/folder'], subtitles=True, embedded_subtitles=True)
User Guide
----------
This part of the documentation details how to use subliminal for most common tasks
# download
subliminal.download_best_subtitles(videos, {Language('eng'), Language('fra')}, age=timedelta(week=1))
See :mod:`subliminal.api`, :func:`~subliminal.video.scan_videos` and :func:`~subliminal.video.scan_video`
How it works
------------
Subliminal makes use of various libraries to achieve its goal:
* `enzyme <http://enzyme.readthedocs.org>`_ to detect embedded subtitles in videos and retrieve metadata
* `guessit <http://guessit.readthedocs.org>`_ to guess informations from filenames
* `babelfish <http://babelfish.readthedocs.org>`_ to work with languages
* `requests <http://docs.python-requests.org>`_ to make human readable HTTP requests
* `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup>`_ to parse HTML and XML
* `dogpile.cache <http://dogpilecache.readthedocs.org>`_ to cache intermediate search data
* `charade <https://github.com/sigmavirus24/charade>`_ to detect subtitles' encoding
* `pysrt <https://github.com/byroot/pysrt>`_ to validate downloaded subtitles
License
-------
MIT
Documentation
-------------
.. toctree::
:maxdepth: 2
user
:maxdepth: 2
Developer Guide
---------------
This part of the documentation explains internal behavior of subliminal and its algorithms
.. toctree::
:maxdepth: 2
dev
provider_guide
API Documentation
-----------------
Most common subliminal features are listed here
If you are looking for information on a specific function, class or method,
this part of the documentation is for you.
.. automodule:: subliminal
:members:
:noindex:
.. toctree::
:maxdepth: 2
api/api
api/cache
api/cli
api/exceptions
api/providers
api/score
api/subtitle
api/video
.. include:: ../HISTORY.rst
+103
View File
@@ -0,0 +1,103 @@
Provider Guide
==============
This guide is going to explain how to add a :class:`~subliminal.providers.Provider` to subliminal
Requirements
------------
When starting a provider you should be able to answer to the following questions:
* What languages does my provider support?
* What are the language codes for the supported languages?
* Does my provider deliver subtitles for episodes? for movies?
* Does my provider require a video hash?
Each response of these questions will help you set the correct attributes for your
:class:`~subliminal.providers.Provider`.
Video Validation
----------------
Not all providers deliver subtitles for :class:`~subliminal.video.Episode`. Some may require a hash.
The :meth:`~subliminal.providers.Provider.check` method does validation against a :class:`~subliminal.video.Video`
object and will return `False` if the given :class:`~subliminal.video.Video` isn't suitable. If you're not happy
with the default implementation, you can override it.
Configuration
-------------
API keys must not be configurable by the user and must remain linked to subliminal. Hence they must be written
in the provider module.
Per-user authentication is allowed and must be configured at instantiation as keyword arguments. Configuration
will be done by the user through the `provider_configs` argument of the :func:`~subliminal.api.list_subtitles` and
:func:`~subliminal.api.download_best_subtitles` functions. No network operation must be done during instantiation,
only configuration. Any error in the configuration must raise a
:class:`~subliminal.exceptions.ProviderConfigurationError`.
Beyond this point, if a network error occurs, a :class:`~subliminal.exceptions.ProviderNotAvailable` exception
must be raised and an unexpected behavior must raise a :class:`~subliminal.exceptions.ProviderError` exception.
Initialization / Termination
----------------------------
Actual authentication operations must take place in the :meth:`~subliminal.providers.Provider.initialize` method.
If you need anything to be executed when the provider isn't used anymore like logout,
use :meth:`~subliminal.providers.Provider.terminate`.
Caching policy
--------------
To save bandwidth and improve querying time, intermediate data should be cached when possible. Typical use case is
when a query to retrieve show ids is required prior to the query to actually search for subtitles. In that case
the function that gets the show id from the show name must be cached.
Language
--------
To be able to handle various language codes, subliminal makes use of `babelfish <http://babelfish.readthedocs.org>`_
Language and converters. You must set the attribute :attr:`~subliminal.providers.Provider.languages` with a set of
supported :class:`babelfish.Language`.
If you cannot find a suitable converter for your provider, you can `make one of your own
<http://babelfish.readthedocs.org/en/latest/#custom-converters>`_.
Querying
--------
The :meth:`~subliminal.providers.Provider.query` method parameters must include all aspects of provider's querying with
simple types.
Subtitle
--------
A custom :class:`~subliminal.subtitle.Subtitle` subclass must be created to represent a subtitle from the provider.
It must have relevant attributes that can be used to compute the matches of the subtitle against a
:class:`~subliminal.video.Video` object.
Score computation
-----------------
To be able to compare subtitles coming from different providers between them, the
:meth:`~subliminal.subtitle.Subtitle.compute_matches` method must be implemented.
If `guessit <http://guessit.readthedocs.org>`_ is used to extract data from the
:class:`~subliminal.subtitle.Subtitle` subclass, you can use :func:`~subliminal.subtitle.compute_guess_matches`
as a helper to compute matches between the :class:`~subliminal.video.Video` and the :class:`guessit.Guess`.
Refer to the `scores` attribute of :class:`~subliminal.video.Episode` and :class:`~subliminal.video.Movie`
for a list of possible matches.
Unittesting
-----------
All possible uses of the :meth:`~subliminal.providers.Provider.query` method must be unittested including the uses
that produce exceptions other than :class:`~subliminal.exceptions.ProviderNotAvailable`.
The :meth:`~subliminal.subtitle.Subtitle.compute_matches` is used to validate the unittests.
As it is not possible to unittest all uses of the :meth:`~subliminal.providers.Provider.list_subtitles`
and :meth:`~subliminal.providers.Provider.download_subtitle` methods, unitests are only required to cover most common
use cases.
See existing unittests for more details on how to proceed.
-118
View File
@@ -1,118 +0,0 @@
There are 4 different ways of using subliminal and each one is described in a dedicated section below.
First, here are some basics
Basics
------
Services
^^^^^^^^
You can use subliminal with multiple services to get the best result.
Current available services are available in the :data:`subliminal.SERVICES` variable.
.. autodata:: subliminal.SERVICES
Languages
^^^^^^^^^
Subliminal supports multiple languages representations based on `ISO-639 <http://en.wikipedia.org/wiki/ISO_639>`_
and `ISO-3166 <http://en.wikipedia.org/wiki/ISO_3166>`_. Any single ISO-639 string or combination of ISO-639 and
ISO-3166 is acceptable. For example, you can use ``pt-br`` for Portuguese (Brazil) or ``en`` for English.
Paths
^^^^^
All paths parameters in subliminal most commont functions can be either *a file path*,
*a file name* or a *folder path*
* File path (existing): hashes of the file will be computed and used during the search for services that
supports this functionnality.
* File name (or non-existing file path): the guessit python library will be used to guess informations
and a text-based search will be done with services.
* Folder path (containing video files): the given folder will be searched for video files using their
:data:`~subliminal.videos.MIMETYPES` and/or :data:`~subliminal.videos.EXTENSIONS`.
The default maximum depth to scan is 3
CLI
---
Subliminal is shipped with a Command Line Interface that allows you to
download subtitles for one or more videos in a multithreaded way.
.. note::
The cache directory defaults to *~/.config/subliminal*. Even on Windows
Usage
^^^^^
You can have the documentation of the CLI using ``subliminal --help``::
usage: subliminal [-h] [-l LG] [-s NAME] [-m] [-f] [-w N] [-a AGE] [-c]
[-q | -v] [--cache-dir DIR | --no-cache-dir] [--version]
PATH [PATH ...]
Subtitles, faster than your thoughts
positional arguments:
PATH path to video file or folder
optional arguments:
-h, --help show this help message and exit
-l LG, --language LG wanted language (ISO 639-1)
-s NAME, --service NAME
service to use
-m, --multi download multiple subtitle languages
-f, --force replace existing subtitle file
-w N, --workers N use N threads (default: 4)
-a AGE, --age AGE scan only for files newer or older (prefix with +)
than AGE (e.g. 12h, 1w2d, +3d6h)
-c, --compatibility try not to use unicode (use this if you have encoding
errors)
-q, --quiet disable output
-v, --verbose verbose output
--cache-dir DIR cache directory to use
--no-cache-dir do not use cache directory (some services may not
work)
--version show program's version number and exit
Cron job
^^^^^^^^
This CLI is well suited for automatic subtitles downloads. For example, to download english and french
subtitles for videos newer than one week under /path/to/videos/ each day at 1:00AM with a single worker,
you can use the following crontab line::
0 1 * * * user /path/to/subliminal -m -l en -l fr -w 1 -a 1w -q /path/to/videos/
Simple module use
-----------------
Subliminal comes with two basic functions to search and download subtitles. For example, you
can do::
>>> subliminal.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
.. autofunction:: subliminal.list_subtitles
Or even download missing subtitles for each episodes under the given folders in two different languages::
>>> subliminal.download_subtitles(['/mnt/videos/BBT/Season 05', '/mnt/videos/HIMYM/Season 07'],
... ['en', 'fr'], force=False, multi=True)
.. autofunction:: subliminal.download_subtitles
Multi-threaded module use
-------------------------
You can call the same functions on a :class:`subliminal.Pool` object previously
created with the appropriate number of workers.
.. autoclass:: subliminal.Pool
:members:
You have to call the :meth:`~subliminal.Pool.start` method before any actions and
:meth:`~subliminal.Pool.stop` before exiting your program::
>>> p = subliminal.Pool(4)
... p.start()
... p.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
... p.stop()
To make the use of :class:`~subliminal.Pool` easier, you can use the ``with`` statement
that takes care of that for you::
>>> with subliminal.Pool(4) as p:
... p.list_subtitles('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4', ['en'])
-1
View File
@@ -1 +0,0 @@
lxml
+9 -5
View File
@@ -1,5 +1,9 @@
beautifulsoup4>=4.0
guessit>=0.4.1
requests
enzyme>=0.1
html5lib
beautifulsoup4>=4.3.2
guessit>=0.6.2
requests>=2.0.1
enzyme>=0.4.0
html5lib>=0.99
dogpile.cache>=0.5.1
babelfish>=0.3.0
charade>=1.0.3
pysrt>=0.5.0
-99
View File
@@ -1,99 +0,0 @@
#!/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/>.
import argparse
import datetime
import logging
import os
import re
import subliminal
import sys
def main():
parser = argparse.ArgumentParser(description='Subtitles, faster than your thoughts')
parser.add_argument('-l', '--language', action='append', dest='languages', help='wanted language (ISO 639-1)', metavar='LG')
parser.add_argument('-s', '--service', action='append', dest='services', help='service to use', metavar='NAME')
parser.add_argument('-m', '--multi', action='store_true', help='download multiple subtitle languages')
parser.add_argument('-f', '--force', action='store_true', help='replace existing subtitle file')
parser.add_argument('-w', '--workers', action='store', help='use N threads (default: %(default)s)', metavar='N', type=int, default=4)
parser.add_argument('-a', '--age', action='store', help='scan only for files newer or older (prefix with +) than AGE (e.g. 12h, 1w2d, +3d6h)', metavar='AGE', default=None)
parser.add_argument('-c', '--compatibility', action='store_true', help='try not to use unicode (use this if you have encoding errors)')
group_verbosity = parser.add_mutually_exclusive_group()
group_verbosity.add_argument('-q', '--quiet', action='store_true', help='disable output')
group_verbosity.add_argument('-v', '--verbose', action='store_true', help='verbose output')
group_cache = parser.add_mutually_exclusive_group()
group_cache.add_argument('--cache-dir', action='store', dest='cache_dir', help='cache directory to use', metavar='DIR', default=os.path.expanduser('~/.config/subliminal'))
group_cache.add_argument('--no-cache-dir', action='store_false', dest='cache_dir', help='do not use cache directory (some services may not work)')
parser.add_argument('--version', action='version', version=subliminal.__version__)
parser.add_argument('paths', nargs='+', help='path to video file or folder', metavar='PATH')
args = parser.parse_args()
# Set log verbosity
if args.verbose:
logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(asctime)s %(name)-24s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
elif not args.quiet:
logging.basicConfig(level=logging.WARN, format='%(levelname)s: %(name)s %(message)s')
# Create cache directory
if not os.path.exists(args.cache_dir):
os.mkdir(args.cache_dir)
# Create filter function
scan_filter = None
if args.age:
regex = re.compile(r'^(?P<sign>\+?)((?P<weeks>\d+?)w)?((?P<days>\d+?)d)?((?P<hours>\d+?)h)?')
parts = regex.match(args.age)
if not parts:
raise ValueError('Incorrect age format')
time_params = {}
parts = parts.groupdict()
for name, param in parts.iteritems():
if param and name != 'sign':
time_params[name] = int(param)
if parts['sign'] == '+':
scan_filter = lambda x: datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(x)) < datetime.timedelta(**time_params)
else:
scan_filter = lambda x: datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(x)) > datetime.timedelta(**time_params)
# Compatibility mode
if args.compatibility:
paths = args.paths
else:
paths = [unicode(x) for x in args.paths]
# Download subtitles
with subliminal.Pool(args.workers) as p:
results = p.download_subtitles(paths, languages=args.languages, services=args.services, cache_dir=args.cache_dir,
force=args.force, multi=args.multi, scan_filter=scan_filter)
if not results:
if not args.quiet:
sys.stderr.write('No subtitles downloaded\n')
exit(1)
if not args.quiet:
print '*' * 50
print 'Downloaded %d subtitle(s) for %d video(s)' % (sum([len(s) for s in results.itervalues()]), len(results))
for _, subtitles in results.iteritems():
for subtitle in subtitles:
print '%s from %s' % (subtitle.path, subtitle.service)
print '*' * 50
if __name__ == '__main__':
main()
+29 -42
View File
@@ -1,53 +1,40 @@
#!/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/>.
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__,
license='LGPLv3',
version='0.7.2',
license='MIT',
description='Subtitles, faster than your thoughts',
long_description=read('README.rst') + '\n\n' + read('NEWS.rst'),
classifiers=['Development Status :: 4 - Beta',
'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
'Intended Audience :: Developers',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Multimedia :: Video'],
long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(),
keywords='subtitle subtitles video movie episode tv show',
url='https://github.com/Diaoul/subliminal',
author='Antoine Bertin',
author_email='diaoulael@gmail.com',
url='https://github.com/Diaoul/subliminal',
packages=find_packages(),
scripts=['scripts/subliminal'],
test_suite='tests.suite',
install_requires=required,
extras_require={'full': ['lxml']})
classifiers=['Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Multimedia :: Video'],
entry_points={
'console_scripts': ['subliminal = subliminal.cli:subliminal'],
'subliminal.providers': ['addic7ed = subliminal.providers.addic7ed:Addic7edProvider',
'bierdopje = subliminal.providers.bierdopje:BierDopjeProvider',
'opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider',
'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider',
'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider',
'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider'],
'babelfish.converters': ['addic7ed = subliminal.converters.addic7ed:Addic7edConverter',
'podnapisi = subliminal.converters.podnapisi:PodnapisiConverter',
'tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter']
},
install_requires=open('requirements.txt').readlines(),
test_suite='subliminal.tests.suite')
+12 -30
View File
@@ -1,34 +1,16 @@
# -*- 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 .api import list_subtitles, download_subtitles
from .async import Pool
from .core import (SERVICES, LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE,
MATCHING_CONFIDENCE)
from .infos import __version__
__title__ = 'subliminal'
__version__ = '0.7.2'
__author__ = 'Antoine Bertin'
__license__ = 'MIT'
__copyright__ = 'Copyright 2013 Antoine Bertin'
import logging
try:
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record):
pass
from .api import PROVIDERS_ENTRY_POINT, list_subtitles, download_subtitles, download_best_subtitles
from .cache import region as cache_region
from .exceptions import Error, ProviderError, ProviderConfigurationError, ProviderNotAvailable, InvalidSubtitle
from .subtitle import Subtitle
from .video import VIDEO_EXTENSIONS, SUBTITLE_EXTENSIONS, Video, Episode, Movie, scan_videos, scan_video
__all__ = ['SERVICES', 'LANGUAGE_INDEX', 'SERVICE_INDEX', 'SERVICE_CONFIDENCE',
'MATCHING_CONFIDENCE', 'list_subtitles', 'download_subtitles', 'Pool']
logging.getLogger(__name__).addHandler(NullHandler())
logging.getLogger(__name__).addHandler(logging.NullHandler())
+258 -91
View File
@@ -1,109 +1,276 @@
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from .core import (SERVICES, LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE,
MATCHING_CONFIDENCE, create_list_tasks, consume_task, create_download_tasks,
group_by_video, key_subtitles)
from .language import language_set, language_list, LANGUAGES
from __future__ import unicode_literals
import collections
import io
import logging
import operator
import babelfish
import pkg_resources
from .exceptions import ProviderNotAvailable, InvalidSubtitle
from .subtitle import get_subtitle_path
__all__ = ['list_subtitles', 'download_subtitles']
logger = logging.getLogger(__name__)
#: Entry point for the providers
PROVIDERS_ENTRY_POINT = 'subliminal.providers'
def list_subtitles(paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None):
"""List subtitles in given paths according to the criteria
:param paths: path(s) to video file or folder
:type paths: string or list
:param languages: languages to search for, in preferred order
:type languages: list of :class:`~subliminal.language.Language` or string
:param list services: services to use for the search, in preferred order
:param bool force: force searching for subtitles even if some are detected
:param bool multi: search multiple languages for the same video
:param string cache_dir: path to the cache directory to use
:param int max_depth: maximum depth for scanning entries
:param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
def list_subtitles(videos, languages, providers=None, provider_configs=None):
"""List subtitles for `videos` with the given `languages` using the specified `providers`
:param videos: videos to list subtitles for
:type videos: set of :class:`~subliminal.video.Video`
:param languages: languages of subtitles to search for
:type languages: set of :class:`babelfish.Language`
:param providers: providers to use for the search, if not all
:type providers: list of string or None
:param provider_configs: configuration for providers
:type provider_configs: dict of provider name => provider constructor kwargs
:return: found subtitles
:rtype: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.ResultSubtitle`]
:rtype: dict of :class:`~subliminal.video.Video` => [:class:`~subliminal.subtitle.Subtitle`]
"""
services = services or SERVICES
languages = language_set(languages) if languages is not None else language_set(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
if any([not isinstance(p, unicode) for p in paths]):
logger.warning(u'Not all entries are unicode')
results = []
service_instances = {}
tasks = create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for task in tasks:
provider_configs = provider_configs or {}
subtitles = collections.defaultdict(list)
# filter videos
videos = [v for v in videos if v.subtitle_languages & languages < languages]
if not videos:
logger.info('No video to download subtitles for with languages %r', languages)
return subtitles
subtitle_languages = set.intersection(*[v.subtitle_languages for v in videos])
for provider_entry_point in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT):
# filter and initialize provider
if providers is not None and provider_entry_point.name not in providers:
logger.debug('Skipping provider %r: not in the list', provider_entry_point.name)
continue
Provider = provider_entry_point.load()
provider_languages = Provider.languages & languages - subtitle_languages
if not provider_languages:
logger.info('Skipping provider %r: no language to search for', provider_entry_point.name)
continue
provider_videos = [v for v in videos if Provider.check(v)]
if not provider_videos:
logger.info('Skipping provider %r: no video to search for', provider_entry_point.name)
continue
# list subtitles with the provider
try:
result = consume_task(task, service_instances)
results.append((task.video, result))
except:
logger.error(u'Error consuming task %r' % task, exc_info=True)
for service_instance in service_instances.itervalues():
service_instance.terminate()
return group_by_video(results)
with Provider(**provider_configs.get(provider_entry_point.name, {})) as provider:
for provider_video in provider_videos:
provider_video_languages = provider_languages - provider_video.subtitle_languages
if not provider_video_languages:
logger.debug('Skipping provider %r: no language to search for for video %r',
provider_entry_point.name, provider_video)
continue
logger.info('Listing subtitles with provider %r for video %r with languages %r',
provider_entry_point.name, provider_video, provider_video_languages)
try:
provider_subtitles = provider.list_subtitles(provider_video, provider_video_languages)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
break
except:
logger.exception('Unexpected error in provider %r', provider_entry_point.name)
continue
logger.info('Found %d subtitles', len(provider_subtitles))
subtitles[provider_video].extend(provider_subtitles)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
return subtitles
def download_subtitles(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
def download_subtitles(subtitles, provider_configs=None, single=False):
"""Download subtitles
:param paths: path(s) to video file or folder
:type paths: string or list
:param languages: languages to search for, in preferred order
:type languages: list of :class:`~subliminal.language.Language` or string
:param list services: services to use for the search, in preferred order
:param bool force: force searching for subtitles even if some are detected
:param bool multi: search multiple languages for the same video
:param string cache_dir: path to the cache directory to use
:param int max_depth: maximum depth for scanning entries
:param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
:param order: preferred order for subtitles sorting
:type list: list of :data:`~subliminal.core.LANGUAGE_INDEX`, :data:`~subliminal.core.SERVICE_INDEX`, :data:`~subliminal.core.SERVICE_CONFIDENCE`, :data:`~subliminal.core.MATCHING_CONFIDENCE`
:return: downloaded subtitles
:rtype: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.ResultSubtitle`]
.. note::
If you use ``multi=True``, :data:`~subliminal.core.LANGUAGE_INDEX` has to be the first item of the ``order`` list
or you might get unexpected results.
:param subtitles: subtitles to download
:type subtitles: dict of :class:`~subliminal.video.Video` => [:class:`~subliminal.subtitle.Subtitle`]
:param provider_configs: configuration for providers
:type provider_configs: dict of provider name => provider constructor kwargs
:param bool single: download with .srt extension if `True`, add language identifier otherwise
"""
services = services or SERVICES
languages = language_list(languages) if languages is not None else language_list(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
order = order or [LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE]
subtitles_by_video = list_subtitles(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for video, subtitles in subtitles_by_video.iteritems():
subtitles.sort(key=lambda s: key_subtitles(s, video, languages, services, order), reverse=True)
results = []
service_instances = {}
tasks = create_download_tasks(subtitles_by_video, languages, multi)
for task in tasks:
provider_configs = provider_configs or {}
discarded_providers = set()
providers_by_name = {ep.name: ep.load() for ep in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT)}
initialized_providers = {}
try:
for video, video_subtitles in subtitles.items():
languages = {subtitle.language for subtitle in video_subtitles}
downloaded_languages = set()
for subtitle in video_subtitles:
# filter
if subtitle.language in downloaded_languages:
continue
if subtitle.provider_name in discarded_providers:
logger.debug('Skipping subtitle from discarded provider %r', subtitle.provider_name)
continue
# initialize provider
if subtitle.provider_name in initialized_providers:
provider = initialized_providers[subtitle.provider_name]
else:
provider = providers_by_name[subtitle.provider_name](**provider_configs.get(subtitle.provider_name, {}))
try:
provider.initialize()
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', subtitle.provider_name)
discarded_providers.add(subtitle.provider_name)
continue
initialized_providers[subtitle.provider_name] = provider
# download subtitles
subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language)
logger.info('Downloading subtitle %r into %r', subtitle, subtitle_path)
try:
subtitle_text = provider.download_subtitle(subtitle)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', subtitle.provider_name)
discarded_providers.add(subtitle.provider_name)
continue
except InvalidSubtitle:
logger.info('Invalid subtitle, skipping it')
continue
except:
logger.exception('Unexpected error in provider %r', subtitle.provider_name)
continue
with io.open(subtitle_path, 'w', encoding='utf-8') as f:
f.write(subtitle_text)
downloaded_languages.add(subtitle.language)
if single or downloaded_languages == languages:
break
finally: # terminate providers
for (provider_name, provider) in initialized_providers.items():
try:
provider.terminate()
except ProviderNotAvailable:
logger.warning('Provider %r is not available, unable to terminate', provider_name)
except:
logger.exception('Unexpected error in provider %r', provider_name)
def download_best_subtitles(videos, languages, providers=None, provider_configs=None, single=False, min_score=0,
hearing_impaired=False):
"""Download the best subtitles for `videos` with the given `languages` using the specified `providers`
:param videos: videos to download subtitles for
:type videos: set of :class:`~subliminal.video.Video`
:param languages: languages of subtitles to download
:type languages: set of :class:`babelfish.Language`
:param providers: providers to use for the search, if not all
:type providers: list of string or None
:param provider_configs: configuration for providers
:type provider_configs: dict of provider name => provider constructor kwargs
:param bool single: download with .srt extension if `True`, add language identifier otherwise
:param int min_score: minimum score for subtitles to download
:param bool hearing_impaired: download hearing impaired subtitles
"""
provider_configs = provider_configs or {}
discarded_providers = set()
downloaded_subtitles = collections.defaultdict(list)
# filter videos
videos = [v for v in videos if v.subtitle_languages & languages < languages
and (not single or babelfish.Language('und') not in v.subtitle_languages)]
if not videos:
logger.info('No video to download subtitles for with languages %r', languages)
return downloaded_subtitles
# filter and initialize providers
subtitle_languages = set.intersection(*[v.subtitle_languages for v in videos])
initialized_providers = {}
for provider_entry_point in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT):
if providers is not None and provider_entry_point.name not in providers:
logger.debug('Skipping provider %r: not in the list', provider_entry_point.name)
continue
Provider = provider_entry_point.load()
if not Provider.languages & languages - subtitle_languages:
logger.info('Skipping provider %r: no language to search for', provider_entry_point.name)
continue
if not [v for v in videos if Provider.check(v)]:
logger.info('Skipping provider %r: no video to search for', provider_entry_point.name)
continue
provider = Provider(**provider_configs.get(provider_entry_point.name, {}))
try:
result = consume_task(task, service_instances)
results.append((task.video, result))
except:
logger.error(u'Error consuming task %r' % task, exc_info=True)
for service_instance in service_instances.itervalues():
service_instance.terminate()
return group_by_video(results)
provider.initialize()
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
continue
initialized_providers[provider_entry_point.name] = provider
try:
for video in videos:
# search for subtitles
subtitles = []
for provider_name, provider in initialized_providers.items():
if provider.check(video):
if provider_name in discarded_providers:
logger.debug('Skipping discarded provider %r', provider_name)
continue
provider_video_languages = provider.languages & languages - video.subtitle_languages
if not provider_video_languages:
logger.debug('Skipping provider %r: no language to search for for video %r', provider_name,
video)
continue
logger.info('Listing subtitles with provider %r for video %r with languages %r',
provider_name, video, provider_video_languages)
try:
provider_subtitles = provider.list_subtitles(video, provider_video_languages)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', provider_name)
discarded_providers.add(provider_name)
continue
except:
logger.exception('Unexpected error in provider %r', provider_name)
continue
logger.info('Found %d subtitles', len(provider_subtitles))
subtitles.extend(provider_subtitles)
# find the best subtitles and download them
downloaded_languages = video.subtitle_languages.copy()
for subtitle, score in sorted([(s, s.compute_score(video)) for s in subtitles],
key=operator.itemgetter(1), reverse=True):
# filter
if subtitle.provider_name in discarded_providers:
logger.debug('Skipping subtitle from discarded provider %r', subtitle.provider_name)
continue
if subtitle.hearing_impaired != hearing_impaired:
logger.debug('Skipping subtitle: hearing impaired != %r', hearing_impaired)
continue
if score < min_score:
logger.debug('Skipping subtitle: score < %d', min_score)
continue
if subtitle.language in downloaded_languages:
logger.debug('Skipping subtitle: %r already downloaded', subtitle.language)
continue
# download
provider = initialized_providers[subtitle.provider_name]
subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language)
logger.info('Downloading subtitle %r with score %d into %r', subtitle, score, subtitle_path)
try:
subtitle_text = provider.download_subtitle(subtitle)
downloaded_subtitles[video].append(subtitle)
except ProviderNotAvailable:
logger.warning('Provider %r is not available, discarding it', subtitle.provider_name)
discarded_providers.add(subtitle.provider_name)
continue
except InvalidSubtitle:
logger.info('Invalid subtitle, skipping it')
continue
except:
logger.exception('Unexpected error in provider %r', subtitle.provider_name)
continue
with io.open(subtitle_path, 'w', encoding='utf-8') as f:
f.write(subtitle_text)
downloaded_languages.add(subtitle.language)
if single or downloaded_languages >= languages:
logger.debug('All languages downloaded')
break
finally: # terminate providers
for (provider_name, provider) in initialized_providers.items():
try:
provider.terminate()
except ProviderNotAvailable:
logger.warning('Provider %r is not available, unable to terminate', provider_name)
except:
logger.exception('Unexpected error in provider %r', provider_name)
return downloaded_subtitles
-142
View File
@@ -1,142 +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/>.
from .core import (consume_task, LANGUAGE_INDEX, SERVICE_INDEX,
SERVICE_CONFIDENCE, MATCHING_CONFIDENCE, SERVICES, create_list_tasks,
create_download_tasks, group_by_video, key_subtitles)
from .language import language_list, language_set, LANGUAGES
from .tasks import StopTask
import Queue
import logging
import threading
__all__ = ['Worker', 'Pool']
logger = logging.getLogger(__name__)
class Worker(threading.Thread):
"""Consume tasks and put the result in the queue"""
def __init__(self, tasks, results):
super(Worker, self).__init__()
self.tasks = tasks
self.results = results
self.services = {}
def run(self):
while 1:
result = []
try:
task = self.tasks.get(block=True)
if isinstance(task, StopTask):
break
result = consume_task(task, self.services)
self.results.put((task.video, result))
except:
logger.error(u'Exception raised in worker %s' % self.name, exc_info=True)
finally:
self.tasks.task_done()
self.terminate()
logger.debug(u'Thread %s terminated' % self.name)
def terminate(self):
"""Terminate instantiated services"""
for service_name, service in self.services.iteritems():
try:
service.terminate()
except:
logger.error(u'Exception raised when terminating service %s' % service_name, exc_info=True)
class Pool(object):
"""Pool of workers"""
def __init__(self, size):
self.tasks = Queue.Queue()
self.results = Queue.Queue()
self.workers = []
for _ in range(size):
self.workers.append(Worker(self.tasks, self.results))
def __enter__(self):
self.start()
return self
def __exit__(self, *args):
self.stop()
self.join()
def start(self):
"""Start workers"""
for worker in self.workers:
worker.start()
def stop(self):
"""Stop workers"""
for _ in self.workers:
self.tasks.put(StopTask())
def join(self):
"""Join the task queue"""
self.tasks.join()
def collect(self):
"""Collect available results
:return: results of tasks
:rtype: list of :class:`~subliminal.tasks.Task`
"""
results = []
while 1:
try:
result = self.results.get(block=False)
results.append(result)
except Queue.Empty:
break
return results
def list_subtitles(self, paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None):
"""See :meth:`subliminal.list_subtitles`"""
services = services or SERVICES
languages = language_set(languages) if languages is not None else language_set(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
if any([not isinstance(p, unicode) for p in paths]):
logger.warning(u'Not all entries are unicode')
tasks = create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for task in tasks:
self.tasks.put(task)
self.join()
results = self.collect()
return group_by_video(results)
def download_subtitles(self, paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None, order=None):
"""See :meth:`subliminal.download_subtitles`"""
services = services or SERVICES
languages = language_list(languages) if languages is not None else language_list(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
order = order or [LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE]
subtitles_by_video = self.list_subtitles(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for video, subtitles in subtitles_by_video.iteritems():
subtitles.sort(key=lambda s: key_subtitles(s, video, languages, services, order), reverse=True)
tasks = create_download_tasks(subtitles_by_video, languages, multi)
for task in tasks:
self.tasks.put(task)
self.join()
results = self.collect()
return group_by_video(results)
+21 -125
View File
@@ -1,134 +1,30 @@
# -*- 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
import inspect
import dogpile.cache
__all__ = ['Cache', 'cachedmethod']
logger = logging.getLogger(__name__)
#: Subliminal's cache version
CACHE_VERSION = 1
class Cache(object):
"""A Cache object contains cached values for methods. It can have
separate internal caches, one for each service
def subliminal_key_generator(namespace, fn, to_str=dogpile.cache.compat.string_type):
"""Add a :data:`CACHE_VERSION` to dogpile.cache's default function_key_generator"""
if namespace is None:
namespace = '%d:%s:%s' % (CACHE_VERSION, fn.__module__, fn.__name__)
else:
namespace = '%d:%s:%s|%s' % (CACHE_VERSION, fn.__module__, fn.__name__, namespace)
"""
def __init__(self, cache_dir):
self.cache_dir = cache_dir
self.cache = defaultdict(dict)
self.lock = threading.RLock()
args = inspect.getargspec(fn)
has_self = args[0] and args[0][0] in ('self', 'cls')
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 generate_key(*args, **kw):
if kw:
raise ValueError('Keyword arguments not supported')
if has_self:
args = args[1:]
return namespace + '|' + ' '.join(map(to_str, args))
return generate_key
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
#: The dogpile.cache region
region = dogpile.cache.make_region(function_key_generator=subliminal_key_generator)
+167
View File
@@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
import argparse
import datetime
import logging
import os
import re
import sys
import babelfish
import guessit
import pkg_resources
from subliminal import (__version__, PROVIDERS_ENTRY_POINT, cache_region, Video, Episode, Movie, scan_videos,
download_best_subtitles)
try:
import colorlog
except ImportError:
colorlog = None
DEFAULT_CACHE_FILE = os.path.join('~', '.config', 'subliminal.cache.dbm')
def subliminal():
parser = argparse.ArgumentParser(prog='subliminal', description='Subtitles, faster than your thoughts',
epilog='Suggestions and bug reports are greatly appreciated: '
'https://github.com/Diaoul/subliminal/issues', add_help=False)
# required arguments
required_arguments_group = parser.add_argument_group('required arguments')
required_arguments_group.add_argument('paths', nargs='+', metavar='PATH', help='path to video file or folder')
required_arguments_group.add_argument('-l', '--languages', nargs='+', required=True, metavar='LANGUAGE',
help='wanted languages as IETF codes e.g. fr, pt-BR, sr-Cyrl ')
# configuration
configuration_group = parser.add_argument_group('configuration')
configuration_group.add_argument('-s', '--single', action='store_true',
help='download without language code in subtitle\'s filename i.e. .srt only')
configuration_group.add_argument('-c', '--cache-file', default=DEFAULT_CACHE_FILE,
help='cache file (default: %(default)s)')
# filtering
filtering_group = parser.add_argument_group('filtering')
providers = [ep.name for ep in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT)]
filtering_group.add_argument('-p', '--providers', nargs='+', metavar='PROVIDER',
help='providers to use (%s)' % ', '.join(providers))
filtering_group.add_argument('-m', '--min-score', type=int,
help='minimum score for subtitles (0-%d for episodes, 0-%d for movies)'
% (Episode.scores['hash'], Movie.scores['hash']))
filtering_group.add_argument('-a', '--age', help='download subtitles for videos newer than AGE e.g. 12h, 1w2d')
filtering_group.add_argument('-h', '--hearing-impaired', action='store_true',
help='download hearing impaired subtitles')
filtering_group.add_argument('-f', '--force', action='store_true',
help='force subtitle download for videos with existing subtitles')
# addic7ed
addic7ed_group = parser.add_argument_group('addic7ed')
addic7ed_group.add_argument('--addic7ed-username', metavar='USERNAME', help='username for addic7ed provider')
addic7ed_group.add_argument('--addic7ed-password', metavar='PASSWORD', help='password for addic7ed provider')
# output
output_group = parser.add_argument_group('output')
output_exclusive_group = output_group.add_mutually_exclusive_group()
output_exclusive_group.add_argument('-q', '--quiet', action='store_true', help='disable output')
output_exclusive_group.add_argument('-v', '--verbose', action='store_true', help='verbose output')
output_group.add_argument('--color', action='store_true', help='add color to console output (requires colorlog)')
# troubleshooting
troubleshooting_group = parser.add_argument_group('troubleshooting')
troubleshooting_group.add_argument('--debug', action='store_true', help='debug output')
troubleshooting_group.add_argument('--version', action='version', version=__version__)
troubleshooting_group.add_argument('--help', action='help', help='show this help message and exit')
# parse args
args = parser.parse_args()
# parse paths
try:
args.paths = [os.path.abspath(os.path.expanduser(p.decode('utf-8'))) for p in args.paths]
except UnicodeDecodeError:
parser.error('argument paths: encodings is not utf-8: %r' % args.paths)
# parse languages
try:
args.languages = {babelfish.Language.fromietf(l) for l in args.languages}
except babelfish.Error:
parser.error('argument -l/--languages: codes are not IETF: %r' % args.languages)
# parse age
if args.age is not None:
match = re.match(r'^(?:(?P<weeks>\d+?)w)?(?:(?P<days>\d+?)d)?(?:(?P<hours>\d+?)h)?$', args.age)
if not match:
parser.error('argument -a/--age: invalid age: %r' % args.age)
args.age = datetime.timedelta(**{k: int(v) for k, v in match.groupdict(0).items()})
# parse cache-file
args.cache_file = os.path.abspath(os.path.expanduser(args.cache_file))
if not os.path.exists(os.path.split(args.cache_file)[0]):
parser.error('argument -c/--cache-file: directory %r for cache file does not exist'
% os.path.split(args.cache_file)[0])
# parse provider configs
provider_configs = {}
if (args.addic7ed_username is not None and args.addic7ed_password is None
or args.addic7ed_username is None and args.addic7ed_password is not None):
parser.error('argument --addic7ed-username/--addic7ed-password: both arguments are required or none')
if args.addic7ed_username is not None and args.addic7ed_password is not None:
provider_configs['addic7ed'] = {'username': args.addic7ed_username, 'password': args.addic7ed_password}
# parse color
if args.color and colorlog is None:
parser.error('argument --color: colorlog required')
# setup output
if args.debug:
handler = logging.StreamHandler()
if args.color:
handler.setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)-8s%(reset)s [%(blue)s%(name)s-%(funcName)s:%(lineno)d%(reset)s] %(message)s',
log_colors=dict(colorlog.default_log_colors.items() + [('DEBUG', 'cyan')])))
else:
handler.setFormatter(logging.Formatter('%(levelname)-8s [%(name)s-%(funcName)s:%(lineno)d] %(message)s'))
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.DEBUG)
elif args.verbose:
handler = logging.StreamHandler()
if args.color:
handler.setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)-8s%(reset)s [%(blue)s%(name)s%(reset)s] %(message)s'))
else:
handler.setFormatter(logging.Formatter('%(levelname)-8s [%(name)s] %(message)s'))
logging.getLogger('subliminal').addHandler(handler)
logging.getLogger('subliminal').setLevel(logging.INFO)
elif not args.quiet:
handler = logging.StreamHandler()
if args.color:
handler.setFormatter(colorlog.ColoredFormatter('[%(log_color)s%(levelname)s%(reset)s] %(message)s'))
else:
handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
logging.getLogger('subliminal.api').addHandler(handler)
logging.getLogger('subliminal.api').setLevel(logging.INFO)
# configure cache
cache_region.configure('dogpile.cache.dbm', expiration_time=datetime.timedelta(days=30),
arguments={'filename': args.cache_file})
# scan videos
videos = scan_videos([p for p in args.paths if os.path.exists(p)], subtitles=not args.force,
embedded_subtitles=not args.force, age=args.age)
# guess videos
videos.extend([Video.fromguess(os.path.split(p)[1], guessit.guess_file_info(p, 'autodetect')) for p in args.paths
if not os.path.exists(p)])
# download best subtitles
subtitles = download_best_subtitles(videos, args.languages, providers=args.providers,
provider_configs=provider_configs, single=args.single,
min_score=args.min_score, hearing_impaired=args.hearing_impaired)
# result output
if not subtitles:
if not args.quiet:
sys.stderr.write('No subtitles downloaded\n')
exit(1)
if not args.quiet:
subtitles_count = sum([len(s) for s in subtitles.values()])
if subtitles_count == 1:
print('%d subtitle downloaded' % subtitles_count)
else:
print('%d subtitles downloaded' % subtitles_count)
View File
+31
View File
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from babelfish.converters.name import NameConverter
class Addic7edConverter(NameConverter):
def __init__(self):
super(Addic7edConverter, self).__init__()
self.from_addic7ed = {'Català': ('cat',), 'Chinese (Simplified)': ('zho',), 'Chinese (Traditional)': ('zho',),
'Euskera': ('eus',), 'Galego': ('glg',), 'Greek': ('ell',), 'Malay': ('msa',),
'Portuguese (Brazilian)': ('por', 'BR'), 'Serbian (Cyrillic)': ('srp', None, 'Cyrl'),
'Serbian (Latin)': ('srp',), 'Spanish (Latin America)': ('spa',),
'Spanish (Spain)': ('spa',)}
self.to_addic7ed = {('cat',): 'Català', ('zho',): 'Chinese (Simplified)', ('eus',): 'Euskera',
('glg',): 'Galego', ('ell',): 'Greek', ('msa',): 'Malay',
('por', 'BR'): 'Portuguese (Brazilian)', ('srp', None, 'Cyrl'): 'Serbian (Cyrillic)'}
self.codes |= set(self.from_addic7ed.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3, country, script) in self.to_addic7ed:
return self.to_addic7ed[(alpha3, country, script)]
if (alpha3, country) in self.to_addic7ed:
return self.to_addic7ed[(alpha3, country)]
if (alpha3,) in self.to_addic7ed:
return self.to_addic7ed[(alpha3,)]
return super(Addic7edConverter, self).convert(alpha3, country, script)
def reverse(self, addic7ed):
if addic7ed in self.from_addic7ed:
return self.from_addic7ed[addic7ed]
return super(Addic7edConverter, self).reverse(addic7ed)
+33
View File
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from babelfish import ReverseConverter, ConvertError, ReverseError
class PodnapisiConverter(ReverseConverter):
def __init__(self):
super(PodnapisiConverter, self).__init__()
self.from_podnapisi = {2: ('eng',), 28: ('spa',), 26: ('pol',), 36: ('srp',), 1: ('slv',), 38: ('hrv',),
9: ('ita',), 8: ('fra',), 48: ('por', 'BR'), 23: ('nld',), 12: ('ara',), 13: ('ron',),
33: ('bul',), 32: ('por',), 16: ('ell',), 15: ('hun',), 31: ('fin',), 30: ('tur',),
7: ('ces',), 25: ('swe',), 27: ('rus',), 24: ('dan',), 22: ('heb',), 51: ('vie',),
52: ('fas',), 5: ('deu',), 14: ('spa', 'AR'), 54: ('ind',), 47: ('srp', None, 'Cyrl'),
3: ('nor',), 20: ('est',), 10: ('bos',), 17: ('zho',), 37: ('slk',), 35: ('mkd',),
11: ('jpn',), 4: ('kor',), 29: ('sqi',), 6: ('isl',), 19: ('lit',), 46: ('ukr',),
44: ('tha',), 53: ('cat',), 56: ('sin',), 21: ('lav',), 40: ('cmn',), 55: ('msa',),
42: ('hin',), 50: ('bel',)}
self.to_podnapisi = {v: k for k, v in self.from_podnapisi.items()}
self.codes = set(self.from_podnapisi.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3,) in self.to_podnapisi:
return self.to_podnapisi[(alpha3,)]
if (alpha3, country) in self.to_podnapisi:
return self.to_podnapisi[(alpha3, country)]
if (alpha3, country, script) in self.to_podnapisi:
return self.to_podnapisi[(alpha3, country, script)]
raise ConvertError(alpha3, country, script)
def reverse(self, podnapisi):
if podnapisi not in self.from_podnapisi:
raise ReverseError(podnapisi)
return self.from_podnapisi[podnapisi]
+24
View File
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from babelfish.converters.alpha2 import Alpha2Converter
class TVsubtitlesConverter(Alpha2Converter):
def __init__(self):
super(TVsubtitlesConverter, self).__init__()
self.from_tvsubtitles = {'br': ('por', 'BR'), 'ua': ('ukr',), 'gr': ('ell',), 'cn': ('zho',), 'jp': ('jpn',),
'cz': ('ces',)}
self.to_tvsubtitles = {v: k for k, v in self.from_tvsubtitles}
self.codes |= set(self.from_tvsubtitles.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3, country) in self.to_tvsubtitles:
return self.to_tvsubtitles[(alpha3, country)]
if (alpha3,) in self.to_tvsubtitles:
return self.to_tvsubtitles[(alpha3,)]
return super(TVsubtitlesConverter, self).convert(alpha3, country, script)
def reverse(self, tvsubtitles):
if tvsubtitles in self.from_tvsubtitles:
return self.from_tvsubtitles[tvsubtitles]
return super(TVsubtitlesConverter, self).reverse(tvsubtitles)
-275
View File
@@ -1,275 +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/>.
from .exceptions import DownloadFailedError
from .services import ServiceConfig
from .tasks import DownloadTask, ListTask
from .utils import get_keywords
from .videos import Episode, Movie, scan
from .language import Language
from collections import defaultdict
from itertools import groupby
import bs4
import guessit
import logging
__all__ = ['SERVICES', 'LANGUAGE_INDEX', 'SERVICE_INDEX', 'SERVICE_CONFIDENCE', 'MATCHING_CONFIDENCE',
'create_list_tasks', 'create_download_tasks', 'consume_task', 'matching_confidence',
'key_subtitles', 'group_by_video']
logger = logging.getLogger(__name__)
SERVICES = ['opensubtitles', 'bierdopje', 'subswiki', 'subtitulos', 'thesubdb', 'addic7ed', 'tvsubtitles']
LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE = range(4)
def create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter):
"""Create a list of :class:`~subliminal.tasks.ListTask` from one or more paths using the given criteria
:param paths: path(s) to video file or folder
:type paths: string or list
:param set languages: languages to search for
:param list services: services to use for the search
:param bool force: force searching for subtitles even if some are detected
:param bool multi: search multiple languages for the same video
:param string cache_dir: path to the cache directory to use
:param int max_depth: maximum depth for scanning entries
:param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
:return: the created tasks
:rtype: list of :class:`~subliminal.tasks.ListTask`
"""
scan_result = []
for p in paths:
scan_result.extend(scan(p, max_depth, scan_filter))
logger.debug(u'Found %d videos in %r with maximum depth %d' % (len(scan_result), paths, max_depth))
tasks = []
config = ServiceConfig(multi, cache_dir)
services = filter_services(services)
for video, detected_subtitles in scan_result:
detected_languages = set(s.language for s in detected_subtitles)
wanted_languages = languages.copy()
if not force and multi:
wanted_languages -= detected_languages
if not wanted_languages:
logger.debug(u'No need to list multi subtitles %r for %r because %r detected' % (languages, video, detected_languages))
continue
if not force and not multi and Language('Undetermined') in detected_languages:
logger.debug(u'No need to list single subtitles %r for %r because one detected' % (languages, video))
continue
logger.debug(u'Listing subtitles %r for %r with services %r' % (wanted_languages, video, services))
for service_name in services:
mod = __import__('services.' + service_name, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
service = mod.Service
if not service.check_validity(video, wanted_languages):
continue
task = ListTask(video, wanted_languages & service.languages, service_name, config)
logger.debug(u'Created task %r' % task)
tasks.append(task)
return tasks
def create_download_tasks(subtitles_by_video, languages, multi):
"""Create a list of :class:`~subliminal.tasks.DownloadTask` from a list results grouped by video
:param subtitles_by_video: :class:`~subliminal.tasks.ListTask` results with ordered subtitles
:type subtitles_by_video: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.Subtitle`]
:param languages: languages in preferred order
:type languages: :class:`~subliminal.language.language_list`
:param bool multi: download multiple languages for the same video
:return: the created tasks
:rtype: list of :class:`~subliminal.tasks.DownloadTask`
"""
tasks = []
for video, subtitles in subtitles_by_video.iteritems():
if not subtitles:
continue
if not multi:
task = DownloadTask(video, list(subtitles))
logger.debug(u'Created task %r' % task)
tasks.append(task)
continue
for _, by_language in groupby(subtitles, lambda s: languages.index(s.language)):
task = DownloadTask(video, list(by_language))
logger.debug(u'Created task %r' % task)
tasks.append(task)
return tasks
def consume_task(task, services=None):
"""Consume a task. If the ``services`` parameter is given, the function will attempt
to get the service from it. In case the service is not in ``services``, it will be initialized
and put in ``services``
:param task: task to consume
:type task: :class:`~subliminal.tasks.ListTask` or :class:`~subliminal.tasks.DownloadTask`
:param dict services: mapping between the service name and an instance of this service
:return: the result of the task
:rtype: list of :class:`~subliminal.subtitles.ResultSubtitle`
"""
if services is None:
services = {}
logger.info(u'Consuming %r' % task)
result = None
if isinstance(task, ListTask):
service = get_service(services, task.service, config=task.config)
result = service.list(task.video, task.languages)
elif isinstance(task, DownloadTask):
for subtitle in task.subtitles:
service = get_service(services, subtitle.service)
try:
service.download(subtitle)
result = [subtitle]
break
except DownloadFailedError:
logger.warning(u'Could not download subtitle %r, trying next' % subtitle)
continue
if result is None:
logger.error(u'No subtitles could be downloaded for video %r' % task.video)
return result
def matching_confidence(video, subtitle):
"""Compute the probability (confidence) that the subtitle matches the video
:param video: video to match
:type video: :class:`~subliminal.videos.Video`
:param subtitle: subtitle to match
:type subtitle: :class:`~subliminal.subtitles.Subtitle`
:return: the matching probability
:rtype: float
"""
guess = guessit.guess_file_info(subtitle.release, 'autodetect')
video_keywords = get_keywords(video.guess)
subtitle_keywords = get_keywords(guess) | subtitle.keywords
logger.debug(u'Video keywords %r - Subtitle keywords %r' % (video_keywords, subtitle_keywords))
replacement = {'keywords': len(video_keywords & subtitle_keywords)}
if isinstance(video, Episode):
replacement.update({'series': 0, 'season': 0, 'episode': 0})
matching_format = '{series:b}{season:b}{episode:b}{keywords:03b}'
best = matching_format.format(series=1, season=1, episode=1, keywords=len(video_keywords))
if guess['type'] in ['episode', 'episodesubtitle']:
if 'series' in guess and guess['series'].lower() == video.series.lower():
replacement['series'] = 1
if 'season' in guess and guess['season'] == video.season:
replacement['season'] = 1
if 'episodeNumber' in guess and guess['episodeNumber'] == video.episode:
replacement['episode'] = 1
elif isinstance(video, Movie):
replacement.update({'title': 0, 'year': 0})
matching_format = '{title:b}{year:b}{keywords:03b}'
best = matching_format.format(title=1, year=1, keywords=len(video_keywords))
if guess['type'] in ['movie', 'moviesubtitle']:
if 'title' in guess and guess['title'].lower() == video.title.lower():
replacement['title'] = 1
if 'year' in guess and guess['year'] == video.year:
replacement['year'] = 1
else:
logger.debug(u'Not able to compute confidence for %r' % video)
return 0.0
logger.debug(u'Found %r' % replacement)
confidence = float(int(matching_format.format(**replacement), 2)) / float(int(best, 2))
logger.info(u'Computed confidence %.4f for %r and %r' % (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
:param subtitle: subtitle to sort
:type subtitle: :class:`~subliminal.subtitles.ResultSubtitle`
:param video: video to match
:type video: :class:`~subliminal.videos.Video`
:param list languages: languages in preferred order
:param list services: services in preferred order
:param order: preferred order for subtitles sorting
:type list: list of :data:`LANGUAGE_INDEX`, :data:`SERVICE_INDEX`, :data:`SERVICE_CONFIDENCE`, :data:`MATCHING_CONFIDENCE`
:return: a key ready to use for subtitles sorting
:rtype: int
"""
key = ''
for sort_item in order:
if sort_item == LANGUAGE_INDEX:
key += '{0:03d}'.format(len(languages) - languages.index(subtitle.language) - 1)
key += '{0:01d}'.format(subtitle.language == languages[languages.index(subtitle.language)])
elif sort_item == SERVICE_INDEX:
key += '{0:02d}'.format(len(services) - services.index(subtitle.service) - 1)
elif sort_item == SERVICE_CONFIDENCE:
key += '{0:04d}'.format(int(subtitle.confidence * 1000))
elif sort_item == MATCHING_CONFIDENCE:
confidence = 0
if subtitle.release:
confidence = matching_confidence(video, subtitle)
key += '{0:04d}'.format(int(confidence * 1000))
return int(key)
def group_by_video(list_results):
"""Group the results of :class:`ListTasks <subliminal.tasks.ListTask>` into a
dictionary of :class:`~subliminal.videos.Video` => :class:`~subliminal.subtitles.Subtitle`
:param list_results:
:type list_results: list of result of :class:`~subliminal.tasks.ListTask`
:return: subtitles grouped by videos
:rtype: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.Subtitle`]
"""
result = defaultdict(list)
for video, subtitles in list_results:
result[video] += subtitles or []
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
+15 -20
View File
@@ -1,20 +1,5 @@
# -*- 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 __future__ import unicode_literals
class Error(Exception):
@@ -22,11 +7,21 @@ class Error(Exception):
pass
class ServiceError(Error):
""""Exception raised by services"""
class ProviderError(Error):
"""Exception raised by providers"""
pass
class DownloadFailedError(Error):
""""Exception raised when a download task has failed in service"""
class ProviderConfigurationError(ProviderError):
"""Exception raised by providers when badly configured"""
pass
class ProviderNotAvailable(ProviderError):
"""Exception raised by providers when unavailable"""
pass
class InvalidSubtitle(ProviderError):
"""Exception raised by providers when the downloaded subtitle is invalid"""
pass
-18
View File
@@ -1,18 +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/>.
__version__ = '0.6.1'
File diff suppressed because it is too large Load Diff
+131
View File
@@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import babelfish
from ..video import Episode, Movie
class Provider(object):
"""Base class for providers
If any configuration is possible for the provider, like credentials, it must take place during instantiation
:param \*\*kwargs: configuration
:raise: :class:`~subliminal.exceptions.ProviderConfigurationError` if there is a configuration error
"""
#: Supported BabelFish languages
languages = set()
#: Supported video types
video_types = (Episode, Movie)
#: Required hash, if any
required_hash = None
def __init__(self, **kwargs):
pass
def __enter__(self):
self.initialize()
return self
def __exit__(self, *args):
self.terminate()
def initialize(self):
"""Initialize the provider
Must be called when starting to work with the provider. This is the place for network initialization
or login operations.
.. note:
This is called automatically if you use the :keyword:`with` statement
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
"""
pass
def terminate(self):
"""Terminate the provider
Must be called when done with the provider. This is the place for network shutdown or logout operations.
.. note:
This is called automatically if you use the :keyword:`with` statement
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
"""
pass
@classmethod
def check(cls, video):
"""Check if the `video` can be processed
The video is considered invalid if not an instance of :attr:`video_types` or if the :attr:`required_hash` is
not present in :attr:`~subliminal.video.Video`'s `hashes` attribute.
:param video: the video to check
:type video: :class:`~subliminal.video.Video`
:return: `True` if the `video` and `languages` are valid, `False` otherwise
:rtype: bool
"""
if not isinstance(video, cls.video_types):
return False
if cls.required_hash is not None and cls.required_hash not in video.hashes:
return False
return True
def query(self, languages, *args, **kwargs):
"""Query the provider for subtitles
This method arguments match as much as possible the actual parameters for querying the provider
:param languages: languages to search for
:type languages: set of :class:`babelfish.Language`
:param \*args: other required arguments
:param \*\*kwargs: other optional arguments
:return: the subtitles
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
:raise: :class:`~subliminal.exceptions.ProviderError` if something unexpected occured
"""
raise NotImplementedError
def list_subtitles(self, video, languages):
"""List subtitles for the `video` with the given `languages`
This is a proxy for the :meth:`query` method. The parameters passed to the :meth:`query` method may
vary depending on the amount of information available in the `video`
:param video: video to list subtitles for
:type video: :class:`~subliminal.video.Video`
:param languages: languages to search for
:type languages: set of :class:`babelfish.Language`
:return: the subtitles
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
:raise: :class:`~subliminal.exceptions.ProviderError` if something unexpected occured
"""
raise NotImplementedError
def download_subtitle(self, subtitle):
"""Download the `subtitle`
:param subtitle: subtitle to download
:type subtitle: :class:`~subliminal.subtitle.Subtitle`
:return: the subtitle text
:rtype: string
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
:raise: :class:`~subliminal.exceptions.InvalidSubtitle` if the downloaded subtitle is invalid
:raise: :class:`~subliminal.exceptions.ProviderError` if something unexpected occured
"""
raise NotImplementedError
def __repr__(self):
return '<%s [%r]>' % (self.__class__.__name__, self.video_types)
+191
View File
@@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import babelfish
import bs4
import charade
import requests
from . import Provider
from .. import __version__
from ..cache import region
from ..exceptions import ProviderConfigurationError, ProviderNotAvailable, InvalidSubtitle
from ..subtitle import Subtitle, is_valid_subtitle
from ..video import Episode
logger = logging.getLogger(__name__)
class Addic7edSubtitle(Subtitle):
provider_name = 'addic7ed'
def __init__(self, language, series, season, episode, title, version, hearing_impaired, download_link, referer):
super(Addic7edSubtitle, self).__init__(language, hearing_impaired)
self.series = series
self.season = season
self.episode = episode
self.title = title
self.version = version
self.download_link = download_link
self.referer = referer
def compute_matches(self, video):
matches = set()
# series
if video.series and self.series == video.series:
matches.add('series')
# season
if video.season and self.season == video.season:
matches.add('season')
# episode
if video.episode and self.episode == video.episode:
matches.add('episode')
# title
if video.title and self.title.lower() == video.title.lower():
matches.add('title')
# release_group
if video.release_group and self.version and video.release_group.lower() in self.version.lower():
matches.add('release_group')
# resolution
if video.resolution and self.version and video.resolution in self.version.lower():
matches.add('resolution')
return matches
class Addic7edProvider(Provider):
languages = {babelfish.Language('por', 'BR')} | {babelfish.Language(l)
for l in ['ara', 'aze', 'ben', 'bos', 'bul', 'cat', 'ces', 'dan', 'deu', 'ell', 'eng', 'eus', 'fas',
'fin', 'fra', 'glg', 'heb', 'hrv', 'hun', 'hye', 'ind', 'ita', 'jpn', 'kor', 'mkd', 'msa',
'nld', 'nor', 'pol', 'por', 'ron', 'rus', 'slk', 'slv', 'spa', 'sqi', 'srp', 'swe', 'tha',
'tur', 'ukr', 'vie', 'zho']}
video_types = (Episode,)
server = 'http://www.addic7ed.com'
def __init__(self, username=None, password=None):
if username is not None and password is None or username is None and password is not None:
raise ProviderConfigurationError('Username and password must be specified')
self.username = username
self.password = password
self.logged_in = False
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
# login
if self.username is not None and self.password is not None:
logger.debug('Logging in')
data = {'username': self.username, 'password': self.password, 'Submit': 'Log in'}
try:
r = self.session.post(self.server + '/dologin.php', data, timeout=10, allow_redirects=False)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code == 302:
logger.info('Logged in')
self.logged_in = True
else:
logger.error('Failed to login')
def terminate(self):
# logout
if self.logged_in:
try:
r = self.session.get(self.server + '/logout.php', timeout=10)
logger.info('Logged out')
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
self.session.close()
def get(self, url, params=None):
"""Make a GET request on `url` with the given parameters
:param string url: part of the URL to reach with the leading slash
:param params: params of the request
:return: the response
:rtype: :class:`bs4.BeautifulSoup`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
try:
r = self.session.get(self.server + url, params=params, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
return bs4.BeautifulSoup(r.content, ['permissive'])
@region.cache_on_arguments()
def get_show_ids(self):
"""Load the shows page with default series to show ids mapping
:return: series to show ids
:rtype: dict
"""
soup = self.get('/shows.php')
show_ids = {}
for html_show in soup.select('td.version > h3 > a[href^="/show/"]'):
show_ids[html_show.string.lower()] = int(html_show['href'][6:])
return show_ids
@region.cache_on_arguments()
def find_show_id(self, series):
"""Find a show id from the series
Use this only if the series is not in the dict returned by :meth:`get_show_ids`
:param string series: series of the episode
:return: the show id, if any
:rtype: int or None
"""
params = {'search': series, 'Submit': 'Search'}
logger.debug('Searching series %r', params)
suggested_shows = self.get('/search.php', params).select('span.titulo > a[href^="/show/"]')
if not suggested_shows:
logger.info('Series %r not found', series)
return None
return int(suggested_shows[0]['href'][6:])
def query(self, series, season):
show_ids = self.get_show_ids()
if series.lower() in show_ids:
show_id = show_ids[series.lower()]
else:
show_id = self.find_show_id(series.lower())
if show_id is None:
return []
params = {'show_id': show_id, 'season': season}
logger.debug('Searching subtitles %r', params)
link = '/show/{show_id}&season={season}'.format(**params)
soup = self.get(link)
subtitles = []
for row in soup('tr', class_='epeven completed'):
cells = row('td')
if cells[5].string != 'Completed':
logger.debug('Skipping incomplete subtitle')
continue
subtitles.append(Addic7edSubtitle(babelfish.Language.fromaddic7ed(cells[3].string), series, season,
int(cells[1].string), cells[2].string, cells[4].string,
bool(cells[6].string), cells[9].a['href'], link))
return subtitles
def list_subtitles(self, video, languages):
return [s for s in self.query(video.series, video.season)
if s.language in languages and s.episode == video.episode]
def download_subtitle(self, subtitle):
try:
r = self.session.get(self.server + subtitle.download_link, timeout=10,
headers={'Referer': self.server + subtitle.referer})
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
if r.headers['Content-Type'] == 'text/html':
raise ProviderNotAvailable('Download limit exceeded')
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+135
View File
@@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import urllib
import babelfish
import charade
import guessit
import requests
import xml.etree.ElementTree
from . import Provider
from .. import __version__
from ..cache import region
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
from ..subtitle import Subtitle, is_valid_subtitle, compute_guess_matches
from ..video import Episode
logger = logging.getLogger(__name__)
class BierDopjeSubtitle(Subtitle):
provider_name = 'bierdopje'
def __init__(self, language, season, episode, tvdb_id, series, filename, download_link):
super(BierDopjeSubtitle, self).__init__(language)
self.season = season
self.episode = episode
self.tvdb_id = tvdb_id
self.series = series
self.filename = filename
self.download_link = download_link
def compute_matches(self, video):
matches = set()
# tvdb_id
if video.tvdb_id and self.tvdb_id == video.tvdb_id:
matches.add('tvdb_id')
# series
if video.series and self.series == video.series:
matches.add('series')
# season
if video.season and self.season == video.season:
matches.add('season')
# episode
if video.episode and self.episode == video.episode:
matches.add('episode')
matches |= compute_guess_matches(video, guessit.guess_episode_info(self.filename + '.mkv'))
return matches
class BierDopjeProvider(Provider):
languages = {babelfish.Language(l) for l in ['eng', 'nld']}
video_types = (Episode,)
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
def terminate(self):
self.session.close()
def get(self, url, **params):
"""Make a GET request on the `url` formatted with `**params`
:param string url: API part of the URL to reach without the leading slash
:param \*\*params: format specs for the `url`
:return: the response
:rtype: :class:`xml.etree.ElementTree.Element`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
try:
r = self.session.get('http://api.bierdopje.com/A2B638AC5D804C2E/' + url.format(**params), timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code == 429:
raise ProviderNotAvailable('Too Many Requests')
elif r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
return xml.etree.ElementTree.fromstring(r.content)
@region.cache_on_arguments()
def find_show_id(self, series):
"""Find the show id from series name
:param string series: series of the episode
:return: show id
:rtype: int
"""
logger.debug('Searching for series %r', series)
root = self.get('FindShowByName/{series}', series=urllib.quote(series))
if root.find('response/status').text == 'false':
logger.info('Series %r not found', series)
return None
return int(root.find('response/results/result[1]/showid').text)
def query(self, language, season, episode, tvdb_id=None, series=None):
params = {'language': language.alpha2, 'season': season, 'episode': episode}
if tvdb_id is not None:
params['showid'] = tvdb_id
params['istvdbid'] = 'true'
elif series is not None:
show_id = self.find_show_id(series)
if show_id is None:
return []
params['showid'] = show_id
params['istvdbid'] = 'false'
else:
raise ValueError('Missing parameter tvdb_id or series')
logger.debug('Searching subtitles %r', params)
root = self.get('GetAllSubsFor/{showid}/{season}/{episode}/{language}/{istvdbid}', **params)
if root.find('response/status').text == 'false':
logger.debug('No subtitle found')
return []
logger.debug('Found subtitles %r', root.find('response/results'))
return [BierDopjeSubtitle(language, season, episode, tvdb_id, series, result.find('filename').text,
result.find('downloadlink').text) for result in root.find('response/results')]
def list_subtitles(self, video, languages):
return [s for l in languages for s in self.query(l, video.season, video.episode, video.tvdb_id, video.series)]
def download_subtitle(self, subtitle):
try:
r = self.session.get(subtitle.download_link, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code == 429:
raise ProviderNotAvailable('Too Many Requests')
elif r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+159
View File
@@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import base64
import logging
import os
import re
import xmlrpclib
import zlib
import babelfish
import charade
import guessit
from . import Provider
from .. import __version__
from ..exceptions import ProviderError, ProviderNotAvailable, InvalidSubtitle
from ..subtitle import Subtitle, is_valid_subtitle, compute_guess_matches
from ..video import Episode, Movie
logger = logging.getLogger(__name__)
class OpenSubtitlesSubtitle(Subtitle):
provider_name = 'opensubtitles'
series_re = re.compile('^"(?P<series_name>.*)" (?P<series_title>.*)$')
def __init__(self, language, hearing_impaired, id, matched_by, movie_kind, hash, movie_name, movie_release_name, movie_year,
movie_imdb_id, series_season, series_episode):
super(OpenSubtitlesSubtitle, self).__init__(language, hearing_impaired)
self.id = id
self.matched_by = matched_by
self.movie_kind = movie_kind
self.hash = hash
self.movie_name = movie_name
self.movie_release_name = movie_release_name
self.movie_year = movie_year
self.movie_imdb_id = movie_imdb_id
self.series_season = series_season
self.series_episode = series_episode
@property
def series_name(self):
return self.series_re.match(self.movie_name).group('series_name')
@property
def series_title(self):
return self.series_re.match(self.movie_name).group('series_title')
def compute_matches(self, video):
matches = set()
# episode
if isinstance(video, Episode) and self.movie_kind == 'episode':
# series
if video.series and self.series_name.lower() == video.series.lower():
matches.add('series')
# season
if video.season and self.series_season == video.season:
matches.add('season')
# episode
if video.episode and self.series_episode == video.episode:
matches.add('episode')
# guess
matches |= compute_guess_matches(video, guessit.guess_episode_info(self.movie_release_name + '.mkv'))
# movie
elif isinstance(video, Movie) and self.movie_kind == 'movie':
# year
if video.year and self.movie_year == video.year:
matches.add('year')
# guess
matches |= compute_guess_matches(video, guessit.guess_movie_info(self.movie_release_name + '.mkv'))
else:
logger.info('%r is not a valid movie_kind for %r', self.movie_kind, video)
return matches
# hash
if 'opensubtitles' in video.hashes and self.hash == video.hashes['opensubtitles']:
matches.add('hash')
# imdb_id
if video.imdb_id and self.movie_imdb_id == video.imdb_id:
matches.add('imdb_id')
# title
if video.title and self.movie_name.lower() == video.title.lower():
matches.add('title')
return matches
class OpenSubtitlesProvider(Provider):
languages = {babelfish.Language.fromopensubtitles(l) for l in babelfish.CONVERTERS['opensubtitles'].codes}
def __init__(self):
self.server = xmlrpclib.ServerProxy('http://api.opensubtitles.org/xml-rpc')
self.token = None
def initialize(self):
try:
response = self.server.LogIn('', '', 'eng', 'subliminal v%s' % __version__)
except xmlrpclib.ProtocolError:
raise ProviderNotAvailable
if response['status'] != '200 OK':
raise ProviderError('Login failed with status %r' % response['status'])
self.token = response['token']
def terminate(self):
try:
response = self.server.LogOut(self.token)
except xmlrpclib.ProtocolError:
raise ProviderNotAvailable
if response['status'] != '200 OK':
raise ProviderError('Logout failed with status %r' % response['status'])
def query(self, languages, hash=None, size=None, imdb_id=None, query=None):
searches = []
if hash and size:
searches.append({'moviehash': hash, 'moviebytesize': str(size)})
if imdb_id:
searches.append({'imdbid': imdb_id})
if query:
searches.append({'query': query})
if not searches:
raise ValueError('One or more parameter missing')
for search in searches:
search['sublanguageid'] = ','.join(l.opensubtitles for l in languages)
logger.debug('Searching subtitles %r', searches)
try:
response = self.server.SearchSubtitles(self.token, searches)
except xmlrpclib.ProtocolError:
raise ProviderNotAvailable
if response['status'] != '200 OK':
raise ProviderError('Search failed with status %r' % response['status'])
if not response['data']:
logger.debug('No subtitle found')
return []
return [OpenSubtitlesSubtitle(babelfish.Language.fromopensubtitles(r['SubLanguageID']),
bool(int(r['SubHearingImpaired'])), r['IDSubtitleFile'], r['MatchedBy'],
r['MovieKind'], r['MovieHash'], r['MovieName'], r['MovieReleaseName'],
int(r['MovieYear']) if r['MovieYear'] else None, int(r['IDMovieImdb']),
int(r['SeriesSeason']) if r['SeriesSeason'] else None,
int(r['SeriesEpisode']) if r['SeriesEpisode'] else None)
for r in response['data']]
def list_subtitles(self, video, languages):
query = None
if ('opensubtitles' not in video.hashes or not video.size) and not video.imdb_id:
query = video.name.split(os.sep)[-1]
return self.query(languages, hash=video.hashes.get('opensubtitles'), size=video.size, imdb_id=video.imdb_id,
query=query)
def download_subtitle(self, subtitle):
try:
response = self.server.DownloadSubtitles(self.token, [subtitle.id])
except xmlrpclib.ProtocolError:
raise ProviderNotAvailable
if response['status'] != '200 OK':
raise ProviderError('Download failed with status %r' % response['status'])
if not response['data']:
raise ProviderError('Nothing to download')
subtitle_bytes = zlib.decompress(base64.b64decode(response['data'][0]['data']), 47)
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+163
View File
@@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import io
import logging
import re
import xml.etree.ElementTree
import zipfile
import babelfish
import bs4
import charade
import guessit
import requests
from . import Provider
from .. import __version__
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
from ..subtitle import Subtitle, is_valid_subtitle, compute_guess_matches
from ..video import Episode, Movie
logger = logging.getLogger(__name__)
class PodnapisiSubtitle(Subtitle):
provider_name = 'podnapisi'
def __init__(self, language, id, releases, hearing_impaired, link, series=None, season=None, episode=None,
title=None, year=None):
super(PodnapisiSubtitle, self).__init__(language, hearing_impaired)
self.id = id
self.releases = releases
self.hearing_impaired = hearing_impaired
self.link = link
self.series = series
self.season = season
self.episode = episode
self.title = title
self.year = year
def compute_matches(self, video):
matches = set()
# episode
if isinstance(video, Episode):
# series
if video.series and self.series.lower() == video.series.lower():
matches.add('series')
# season
if video.season and self.season == video.season:
matches.add('season')
# episode
if video.episode and self.episode == video.episode:
matches.add('episode')
# guess
for release in self.releases:
matches |= compute_guess_matches(video, guessit.guess_episode_info(release + '.mkv'))
# movie
elif isinstance(video, Movie):
# title
if video.title and self.title.lower() == video.title.lower():
matches.add('title')
# year
if video.year and self.year == video.year:
matches.add('year')
# guess
for release in self.releases:
matches |= compute_guess_matches(video, guessit.guess_movie_info(release + '.mkv'))
return matches
class PodnapisiProvider(Provider):
languages = {babelfish.Language.frompodnapisi(l) for l in babelfish.CONVERTERS['podnapisi'].codes}
video_types = (Episode, Movie)
server = 'http://simple.podnapisi.net'
link_re = re.compile('^.*(?P<link>/ppodnapisi/download/i/\d+/k/.*$)')
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
def terminate(self):
self.session.close()
def get(self, url, params=None, is_xml=True):
"""Make a GET request on `url` with the given parameters
:param string url: part of the URL to reach with the leading slash
:param dict params: params of the request
:param bool xml: whether the response content is XML or not
:return: the response
:rtype: :class:`xml.etree.ElementTree.Element` or :class:`bs4.BeautifulSoup`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
try:
r = self.session.get(self.server + '/ppodnapisi' + url, params=params, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
if is_xml:
return xml.etree.ElementTree.fromstring(r.content)
else:
return bs4.BeautifulSoup(r.content, ['permissive'])
def query(self, language, series=None, season=None, episode=None, title=None, year=None):
params = {'sXML': 1, 'sJ': language.podnapisi}
if series and season and episode:
params['sK'] = series
params['sTS'] = season
params['sTE'] = episode
elif title:
params['sK'] = title
if year:
params['sY'] = year
else:
raise ValueError('Missing parameters series and season and episode or title')
logger.debug('Searching episode %r', params)
subtitles = []
while True:
root = self.get('/search', params)
if not int(root.find('pagination/results').text):
logger.debug('No subtitle found')
break
if series and season and episode:
subtitles.extend([PodnapisiSubtitle(language, int(s.find('id').text), s.find('release').text.split(),
'h' in (s.find('flags').text or ''), s.find('url').text[38:],
series=series, season=season, episode=episode)
for s in root.findall('subtitle')])
elif title:
subtitles.extend([PodnapisiSubtitle(language, int(s.find('id').text), s.find('release').text.split(),
'h' in (s.find('flags').text or ''), s.find('url').text[38:],
title=title, year=year)
for s in root.findall('subtitle')])
if int(root.find('pagination/current').text) >= int(root.find('pagination/count').text):
break
params['page'] = int(root.find('pagination/current').text) + 1
return subtitles
def list_subtitles(self, video, languages):
if isinstance(video, Episode):
return [s for l in languages for s in self.query(l, series=video.series, season=video.season,
episode=video.episode)]
elif isinstance(video, Movie):
return [s for l in languages for s in self.query(l, title=video.title, year=video.year)]
def download_subtitle(self, subtitle):
soup = self.get(subtitle.link, is_xml=False)
link = soup.find('a', href=self.link_re)
if not link:
raise ProviderError('Cannot find the download link')
try:
r = self.session.get(self.server + self.link_re.match(link['href']).group('link'), timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
with zipfile.ZipFile(io.BytesIO(r.content)) as zf:
if len(zf.namelist()) > 1:
raise ProviderError('More than one file to unzip')
subtitle_bytes = zf.read(zf.namelist()[0])
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+81
View File
@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import babelfish
import charade
import requests
from . import Provider
from .. import __version__
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
from ..subtitle import Subtitle, is_valid_subtitle
logger = logging.getLogger(__name__)
class TheSubDBSubtitle(Subtitle):
provider_name = 'thesubdb'
def __init__(self, language, hash):
super(TheSubDBSubtitle, self).__init__(language)
self.hash = hash
def compute_matches(self, video):
matches = set()
# hash
if 'thesubdb' in video.hashes and video.hashes['thesubdb'] == self.hash:
matches.add('hash')
return matches
class TheSubDBProvider(Provider):
languages = {babelfish.Language.fromalpha2(l) for l in ['en', 'es', 'fr', 'it', 'nl', 'pl', 'pt', 'ro', 'sv', 'tr']}
required_hash = 'thesubdb'
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'SubDB/1.0 (subliminal/%s; https://github.com/Diaoul/subliminal)' %
__version__}
def terminate(self):
self.session.close()
def get(self, params):
"""Make a GET request on the server with the given parameters
:param params: params of the request
:return: the response
:rtype: :class:`requests.Response`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
try:
r = self.session.get('http://api.thesubdb.com', params=params, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
return r
def query(self, hash):
params = {'action': 'search', 'hash': hash}
logger.debug('Searching subtitles %r', params)
r = self.get(params)
if r.status_code == 404:
logger.debug('No subtitle found')
return []
elif r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
return [TheSubDBSubtitle(language, hash) for language in
{babelfish.Language.fromalpha2(l) for l in r.content.split(',')}]
def list_subtitles(self, video, languages):
return [s for s in self.query(video.hashes['thesubdb']) if s.language in languages]
def download_subtitle(self, subtitle):
params = {'action': 'download', 'hash': subtitle.hash, 'language': subtitle.language.alpha2}
r = self.get(params)
if r.status_code != 200:
raise ProviderError('Request failed with status code %d' % r.status_code)
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+175
View File
@@ -0,0 +1,175 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import io
import logging
import re
import zipfile
import babelfish
import bs4
import charade
import requests
from . import Provider
from .. import __version__
from ..cache import region
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
from ..subtitle import Subtitle, is_valid_subtitle
from ..video import Episode
logger = logging.getLogger(__name__)
class TVsubtitlesSubtitle(Subtitle):
provider_name = 'tvsubtitles'
def __init__(self, language, series, season, episode, id, rip, release):
super(TVsubtitlesSubtitle, self).__init__(language)
self.series = series
self.season = season
self.episode = episode
self.id = id
self.rip = rip
self.release = release
def compute_matches(self, video):
matches = set()
# series
if video.series and self.series == video.series:
matches.add('series')
# season
if video.season and self.season == video.season:
matches.add('season')
# episode
if video.episode and self.episode == video.episode:
matches.add('episode')
# release_group
if video.release_group and self.release and video.release_group.lower() in self.release.lower():
matches.add('release_group')
# video_codec
if video.video_codec and self.release and (video.video_codec in self.release.lower()
or video.video_codec == 'h264' and 'x264' in self.release.lower()):
matches.add('video_codec')
# resolution
if video.resolution and self.rip and video.resolution in self.rip.lower():
matches.add('resolution')
return matches
class TVsubtitlesProvider(Provider):
languages = {babelfish.Language('por', 'BR')} | {babelfish.Language(l)
for l in ['ara', 'bul', 'ces', 'dan', 'deu', 'ell', 'eng', 'fin', 'fra', 'hun', 'ita', 'jpn', 'kor',
'nld', 'pol', 'por', 'ron', 'rus', 'spa', 'swe', 'tur', 'ukr', 'zho']}
video_types = (Episode,)
server = 'http://www.tvsubtitles.net'
episode_id_re = re.compile('^episode-\d+\.html$')
subtitle_re = re.compile('^\/subtitle-\d+\.html$')
link_re = re.compile('^(?P<series>.+) \(\d{4}-\d{4}\)$')
def initialize(self):
self.session = requests.Session()
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
def terminate(self):
self.session.close()
def request(self, url, params=None, data=None, method='GET'):
"""Make a `method` request on `url` with the given parameters
:param string url: part of the URL to reach with the leading slash
:param dict params: params of the request
:param dict data: data of the request
:param string method: method of the request
:return: the response
:rtype: :class:`bs4.BeautifulSoup`
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
"""
try:
r = self.session.request(method, self.server + url, params=params, data=data, timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
return bs4.BeautifulSoup(r.content, ['permissive'])
@region.cache_on_arguments()
def find_show_id(self, series):
"""Find a show id from the series
:param string series: series of the episode
:return: the show id, if any
:rtype: int or None
"""
data = {'q': series}
logger.debug('Searching series %r', data)
soup = self.request('/search.php', data=data, method='POST')
links = soup.select('div.left li div a[href^="/tvshow-"]')
if not links:
logger.info('Series %r not found', series)
return None
for link in links:
match = self.link_re.match(link.string)
if not match:
logger.warning('Could not parse %r', link.string)
continue
if match.group('series').lower().replace('.', ' ').strip() == series.lower():
return int(link['href'][8:-5])
return int(links[0]['href'][8:-5])
@region.cache_on_arguments()
def find_episode_ids(self, show_id, season):
"""Find episode ids from the show id and the season
:param int show_id: show id
:param int season: season of the episode
:return: episode ids per episode number
:rtype: dict
"""
params = {'show_id': show_id, 'season': season}
logger.debug('Searching episodes %r', params)
soup = self.request('/tvshow-{show_id}-{season}.html'.format(**params))
episode_ids = {}
for row in soup.select('table#table5 tr'):
if not row('a', href=self.episode_id_re):
continue
cells = row('td')
episode_ids[int(cells[0].string.split('x')[1])] = int(cells[1].a['href'][8:-5])
return episode_ids
def query(self, series, season, episode):
show_id = self.find_show_id(series.lower())
if show_id is None:
return []
episode_ids = self.find_episode_ids(show_id, season)
if episode not in episode_ids:
logger.info('Episode %d not found', episode)
return []
params = {'episode_id': episode_ids[episode]}
logger.debug('Searching episode %r', params)
soup = self.request('/episode-{episode_id}.html'.format(**params))
return [TVsubtitlesSubtitle(babelfish.Language.fromtvsubtitles(row.h5.img['src'][13:-4]), series, season,
episode, row['href'][10:-5], row.find('p', title='rip').text.strip() or None,
row.find('p', title='release').text.strip() or None)
for row in soup('a', href=self.subtitle_re)]
def list_subtitles(self, video, languages):
return [s for s in self.query(video.series, video.season, video.episode) if s.language in languages]
def download_subtitle(self, subtitle):
try:
r = self.session.get(self.server + '/download-{subtitle_id}.html'.format(subtitle_id=subtitle.id),
timeout=10)
except requests.Timeout:
raise ProviderNotAvailable('Timeout after 10 seconds')
if r.status_code != 200:
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
with zipfile.ZipFile(io.BytesIO(r.content)) as zf:
if len(zf.namelist()) > 1:
raise ProviderError('More than one file to unzip')
subtitle_bytes = zf.read(zf.namelist()[0])
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'], 'replace')
if not is_valid_subtitle(subtitle_text):
raise InvalidSubtitle
return subtitle_text
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals
from sympy import Eq, symbols, solve
# Symbols
release_group, resolution, video_codec, audio_codec = symbols('release_group resolution video_codec audio_codec')
imdb_id, hash, title, series, tvdb_id, season, episode = symbols('imdb_id hash title series tvdb_id season episode')
year = symbols('year')
def get_episode_equations():
"""Get the score equations for a :class:`~subliminal.video.Episode`
The equations are the following:
1. hash = resolution + video_codec + audio_codec + series + season + episode + release_group
2. series = resolution + video_codec + audio_codec + season + episode + 1
3. tvdb_id = series
4. season = resolution + video_codec + audio_codec + 1
5. imdb_id = series + season + episode
6. resolution = video_codec
7. video_codec = 2 * audio_codec
8. title = season + episode
9. season = episode
10. release_group = season
11. audio_codec = 1
:return: the score equations for an episode
:rtype: list of :class:`sympy.Eq`
"""
equations = []
equations.append(Eq(hash, resolution + video_codec + audio_codec + series + season + episode + release_group))
equations.append(Eq(series, resolution + video_codec + audio_codec + season + episode + release_group))
equations.append(Eq(tvdb_id, series))
equations.append(Eq(season, resolution + video_codec + audio_codec + 1))
equations.append(Eq(imdb_id, series + season + episode))
equations.append(Eq(resolution, video_codec))
equations.append(Eq(video_codec, 2 * audio_codec))
equations.append(Eq(title, season + episode))
equations.append(Eq(season, episode))
equations.append(Eq(release_group, season))
equations.append(Eq(audio_codec, 1))
return equations
def get_movie_equations():
"""Get the score equations for a :class:`~subliminal.video.Movie`
The equations are the following:
1. hash = resolution + video_codec + audio_codec + title + year + release_group
2. imdb_id = hash
3. resolution = video_codec
4. video_codec = 2 * audio_codec
5. title = resolution + video_codec + audio_codec + year + 1
6. release_group = resolution + video_codec + audio_codec + 1
7. year = release_group + 1
8. audio_codec = 1
:return: the score equations for a movie
:rtype: list of :class:`sympy.Eq`
"""
equations = []
equations.append(Eq(hash, resolution + video_codec + audio_codec + title + year + release_group))
equations.append(Eq(imdb_id, hash))
equations.append(Eq(resolution, video_codec))
equations.append(Eq(video_codec, 2 * audio_codec))
equations.append(Eq(title, resolution + video_codec + audio_codec + year + 1))
equations.append(Eq(video_codec, 2 * audio_codec))
equations.append(Eq(release_group, resolution + video_codec + audio_codec + 1))
equations.append(Eq(year, release_group + 1))
equations.append(Eq(audio_codec, 1))
return equations
if __name__ == '__main__':
print(solve(get_episode_equations(), [release_group, resolution, video_codec, audio_codec, imdb_id,
hash, series, tvdb_id, season, episode, title]))
print(solve(get_movie_equations(), [release_group, resolution, video_codec, audio_codec, imdb_id,
hash, title, year]))
-256
View File
@@ -1,256 +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/>.
from ..cache import Cache
from ..exceptions import DownloadFailedError, ServiceError
from ..language import language_set, Language
from ..subtitles import EXTENSIONS
import logging
import os
import requests
import threading
import zipfile
__all__ = ['ServiceBase', 'ServiceConfig']
logger = logging.getLogger(__name__)
class ServiceBase(object):
"""Service base class
:param config: service configuration
:type config: :class:`ServiceConfig`
"""
#: URL to the service server
server_url = ''
#: User Agent for any HTTP-based requests
user_agent = 'subliminal v0.6'
#: Whether based on an API or not
api_based = False
#: Timeout for web requests
timeout = 5
#: :class:`~subliminal.language.language_set` of available languages
languages = language_set()
#: Map between language objects and language codes used in the service
language_map = {}
#: Default attribute of a :class:`~subliminal.language.Language` to get with :meth:`get_code`
language_code = 'alpha2'
#: Accepted video classes (:class:`~subliminal.videos.Episode`, :class:`~subliminal.videos.Movie`, :class:`~subliminal.videos.UnknownVideo`)
videos = []
#: Whether the video has to exist or not
require_video = False
#: List of required features for BeautifulSoup
required_features = None
def __init__(self, config=None):
self.config = config or ServiceConfig()
self.session = None
def __enter__(self):
self.init()
return self
def __exit__(self, *args):
self.terminate()
def init(self):
"""Initialize connection"""
logger.debug(u'Initializing %s' % self.__class__.__name__)
self.session = requests.session(timeout=10, headers={'User-Agent': self.user_agent})
def init_cache(self):
"""Initialize cache, make sure it is loaded from disk"""
if not self.config or not self.config.cache:
raise ServiceError('Cache directory is required')
self.config.cache.load(self.__class__.__name__)
def save_cache(self):
self.config.cache.save(self.__class__.__name__)
def clear_cache(self):
self.config.cache.clear(self.__class__.__name__)
def cache_for(self, func, args, result):
return self.config.cache.cache_for(self.__class__.__name__, func, args, result)
def cached_value(self, func, args):
return self.config.cache.cached_value(self.__class__.__name__, func, args)
def terminate(self):
"""Terminate connection"""
logger.debug(u'Terminating %s' % self.__class__.__name__)
def get_code(self, language):
"""Get the service code for a :class:`~subliminal.language.Language`
It uses the :data:`language_map` and if there's no match, falls back
on the :data:`language_code` attribute of the given :class:`~subliminal.language.Language`
"""
if language in self.language_map:
return self.language_map[language]
if self.language_code is None:
raise ValueError('%r has no matching code' % language)
return getattr(language, self.language_code)
def get_language(self, code):
"""Get a :class:`~subliminal.language.Language` from a service code
It uses the :data:`language_map` and if there's no match, uses the
given code as ``language`` parameter for the :class:`~subliminal.language.Language`
constructor
.. note::
A warning is emitted if the generated :class:`~subliminal.language.Language`
is "Undetermined"
"""
if code in self.language_map:
return self.language_map[code]
language = Language(code, strict=False)
if language == Language('Undetermined'):
logger.warning(u'Code %s could not be identified as a language for %s' % (code, self.__class__.__name__))
return language
def query(self, *args):
"""Make the actual query"""
raise NotImplementedError()
def list(self, video, languages):
"""List subtitles
As a service writer, you can either override this method or implement
:meth:`list_checked` instead to have the languages pre-filtered for you
"""
if not self.check_validity(video, languages):
return []
return self.list_checked(video, languages)
def list_checked(self, video, languages):
"""List subtitles without having to check parameters for validity"""
raise NotImplementedError()
def download(self, subtitle):
"""Download a subtitle"""
self.download_file(subtitle.link, subtitle.path)
return subtitle
@classmethod
def check_validity(cls, video, languages):
"""Check for video and languages validity in the Service
:param video: the video to check
:type video: :class:`~subliminal.videos.video`
:param languages: languages to check
:type languages: :class:`~subliminal.language.Language`
:rtype: bool
"""
languages = (languages & cls.languages) - language_set(['Undetermined'])
if not languages:
logger.debug(u'No language available for service %s' % cls.__name__.lower())
return False
if cls.require_video and not video.exists or not isinstance(video, tuple(cls.videos)):
logger.debug(u'%r is not valid for service %s' % (video, cls.__name__.lower()))
return False
return True
def download_file(self, url, filepath):
"""Attempt to download a file and remove it in case of failure
:param string url: URL to download
:param string filepath: destination path
"""
logger.info(u'Downloading %s in %s' % (url, filepath))
try:
r = self.session.get(url, headers={'Referer': url, 'User-Agent': self.user_agent})
with open(filepath, 'wb') as f:
f.write(r.content)
except Exception as e:
logger.error(u'Download failed: %s' % e)
if os.path.exists(filepath):
os.remove(filepath)
raise DownloadFailedError(str(e))
logger.debug(u'Download finished')
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 in %s' % (url, filepath))
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')
with zipfile.ZipFile(zippath) as zipsub:
for subfile in zipsub.namelist():
if os.path.splitext(subfile)[1] in EXTENSIONS:
with open(filepath, 'w') as f:
f.write(zipsub.open(subfile).read())
break
else:
raise DownloadFailedError('No subtitles found in zip file')
os.remove(zippath)
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))
logger.debug(u'Download finished')
class ServiceConfig(object):
"""Configuration for any :class:`Service`
:param bool multi: whether to download one subtitle per language or not
:param string cache_dir: cache directory
"""
def __init__(self, multi=False, cache_dir=None):
self.multi = multi
self.cache_dir = cache_dir
self.cache = None
if cache_dir is not None:
self.cache = Cache(cache_dir)
def __repr__(self):
return 'ServiceConfig(%r, %s)' % (self.multi, self.cache.cache_dir)
-173
View File
@@ -1,173 +0,0 @@
# -*- 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 ..exceptions import DownloadFailedError
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 os
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'),
'Spanish (Latin America)': Language('spa'), }
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
def download(self, subtitle):
logger.info(u'Downloading %s in %s' % (subtitle.link, subtitle.path))
try:
r = self.session.get(subtitle.link, headers={'Referer': subtitle.link, 'User-Agent': self.user_agent})
soup = BeautifulSoup(r.content, self.required_features)
if soup.title is not None and u'Addic7ed.com' in soup.title.text.strip():
raise DownloadFailedError('Download limit exceeded')
with open(subtitle.path, 'wb') as f:
f.write(r.content)
except Exception as e:
logger.error(u'Download failed: %s' % e)
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
raise DownloadFailedError(str(e))
logger.debug(u'Download finished')
return subtitle
Service = Addic7ed
-102
View File
@@ -1,102 +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/>.
from . import ServiceBase
from ..cache import cachedmethod
from ..exceptions import ServiceError
from ..language import language_set
from ..subtitles import get_subtitle_path, ResultSubtitle, EXTENSIONS
from ..utils import to_unicode
from ..videos import Episode
from bs4 import BeautifulSoup
import logging
import urllib
try:
import cPickle as pickle
except ImportError:
import pickle
logger = logging.getLogger(__name__)
class BierDopje(ServiceBase):
server_url = 'http://api.bierdopje.com/A2B638AC5D804C2E/'
api_based = True
languages = language_set(['eng', 'dut'])
videos = [Episode]
require_video = False
required_features = ['xml']
@cachedmethod
def get_show_id(self, series):
r = self.session.get('%sGetShowByName/%s' % (self.server_url, urllib.quote(series.lower())))
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return None
soup = BeautifulSoup(r.content, self.required_features)
if soup.status.contents[0] == 'false':
logger.debug(u'Could not find show %s' % series)
return None
return int(soup.showid.contents[0])
def load_cache(self):
logger.debug(u'Loading showids from cache...')
with self.lock:
with open(self.showids_cache, 'r') as f:
self.showids = pickle.load(f)
def query(self, filepath, season, episode, languages, tvdbid=None, series=None):
self.init_cache()
if series:
request_id = self.get_show_id(series.lower())
if request_id is None:
return []
request_source = 'showid'
request_is_tvdbid = 'false'
elif tvdbid:
request_id = tvdbid
request_source = 'tvdbid'
request_is_tvdbid = 'true'
else:
raise ServiceError('One or more parameter missing')
subtitles = []
for language in languages:
logger.debug(u'Getting subtitles for %s %d season %d episode %d with language %s' % (request_source, request_id, season, episode, language.alpha2))
r = self.session.get('%sGetAllSubsFor/%s/%s/%s/%s/%s' % (self.server_url, request_id, season, episode, language.alpha2, request_is_tvdbid))
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
soup = BeautifulSoup(r.content, self.required_features)
if soup.status.contents[0] == 'false':
logger.debug(u'Could not find subtitles for %s %d season %d episode %d with language %s' % (request_source, request_id, season, episode, language.alpha2))
continue
path = get_subtitle_path(filepath, language, self.config.multi)
for result in soup.results('result'):
release = to_unicode(result.filename.contents[0])
if not release.endswith(tuple(EXTENSIONS)):
release += '.srt'
subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), result.downloadlink.contents[0],
release=release)
subtitles.append(subtitle)
return subtitles
def list_checked(self, video, languages):
return self.query(video.path or video.release, video.season, video.episode, languages, video.tvdbid, video.series)
Service = BierDopje
-158
View File
@@ -1,158 +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/>.
from . import ServiceBase
from ..exceptions import ServiceError, DownloadFailedError
from ..language import Language, language_set
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..utils import to_unicode
from ..videos import Episode, Movie
import gzip
import logging
import os.path
import xmlrpclib
logger = logging.getLogger(__name__)
class OpenSubtitles(ServiceBase):
server_url = 'http://api.opensubtitles.org/xml-rpc'
api_based = True
# Source: http://www.opensubtitles.org/addons/export_languages.php
languages = language_set(['aar', 'abk', 'ace', 'ach', 'ada', 'ady', 'afa', 'afh', 'afr', 'ain', 'aka', 'akk',
'alb', 'ale', 'alg', 'alt', 'amh', 'ang', 'apa', 'ara', 'arc', 'arg', 'arm', 'arn',
'arp', 'art', 'arw', 'asm', 'ast', 'ath', 'aus', 'ava', 'ave', 'awa', 'aym', 'aze',
'bad', 'bai', 'bak', 'bal', 'bam', 'ban', 'baq', 'bas', 'bat', 'bej', 'bel', 'bem',
'ben', 'ber', 'bho', 'bih', 'bik', 'bin', 'bis', 'bla', 'bnt', 'bos', 'bra', 'bre',
'btk', 'bua', 'bug', 'bul', 'bur', 'byn', 'cad', 'cai', 'car', 'cat', 'cau', 'ceb',
'cel', 'cha', 'chb', 'che', 'chg', 'chi', 'chk', 'chm', 'chn', 'cho', 'chp', 'chr',
'chu', 'chv', 'chy', 'cmc', 'cop', 'cor', 'cos', 'cpe', 'cpf', 'cpp', 'cre', 'crh',
'crp', 'csb', 'cus', 'cze', 'dak', 'dan', 'dar', 'day', 'del', 'den', 'dgr', 'din',
'div', 'doi', 'dra', 'dua', 'dum', 'dut', 'dyu', 'dzo', 'efi', 'egy', 'eka', 'ell',
'elx', 'eng', 'enm', 'epo', 'est', 'ewe', 'ewo', 'fan', 'fao', 'fat', 'fij', 'fil',
'fin', 'fiu', 'fon', 'fre', 'frm', 'fro', 'fry', 'ful', 'fur', 'gaa', 'gay', 'gba',
'gem', 'geo', 'ger', 'gez', 'gil', 'gla', 'gle', 'glg', 'glv', 'gmh', 'goh', 'gon',
'gor', 'got', 'grb', 'grc', 'grn', 'guj', 'gwi', 'hai', 'hat', 'hau', 'haw', 'heb',
'her', 'hil', 'him', 'hin', 'hit', 'hmn', 'hmo', 'hrv', 'hun', 'hup', 'iba', 'ibo',
'ice', 'ido', 'iii', 'ijo', 'iku', 'ile', 'ilo', 'ina', 'inc', 'ind', 'ine', 'inh',
'ipk', 'ira', 'iro', 'ita', 'jav', 'jpn', 'jpr', 'jrb', 'kaa', 'kab', 'kac', 'kal',
'kam', 'kan', 'kar', 'kas', 'kau', 'kaw', 'kaz', 'kbd', 'kha', 'khi', 'khm', 'kho',
'kik', 'kin', 'kir', 'kmb', 'kok', 'kom', 'kon', 'kor', 'kos', 'kpe', 'krc', 'kro',
'kru', 'kua', 'kum', 'kur', 'kut', 'lad', 'lah', 'lam', 'lao', 'lat', 'lav', 'lez',
'lim', 'lin', 'lit', 'lol', 'loz', 'ltz', 'lua', 'lub', 'lug', 'lui', 'lun', 'luo',
'lus', 'mac', 'mad', 'mag', 'mah', 'mai', 'mak', 'mal', 'man', 'mao', 'map', 'mar',
'mas', 'may', 'mdf', 'mdr', 'men', 'mga', 'mic', 'min', 'mkh', 'mlg', 'mlt', 'mnc',
'mni', 'mno', 'moh', 'mon', 'mos', 'mun', 'mus', 'mwl', 'mwr', 'myn', 'myv', 'nah',
'nai', 'nap', 'nau', 'nav', 'nbl', 'nde', 'ndo', 'nds', 'nep', 'new', 'nia', 'nic',
'niu', 'nno', 'nob', 'nog', 'non', 'nor', 'nso', 'nub', 'nwc', 'nya', 'nym', 'nyn',
'nyo', 'nzi', 'oci', 'oji', 'ori', 'orm', 'osa', 'oss', 'ota', 'oto', 'paa', 'pag',
'pal', 'pam', 'pan', 'pap', 'pau', 'peo', 'per', 'phi', 'phn', 'pli', 'pol', 'pon',
'por', 'pra', 'pro', 'pus', 'que', 'raj', 'rap', 'rar', 'roa', 'roh', 'rom', 'rum',
'run', 'rup', 'rus', 'sad', 'sag', 'sah', 'sai', 'sal', 'sam', 'san', 'sas', 'sat',
'scn', 'sco', 'sel', 'sem', 'sga', 'sgn', 'shn', 'sid', 'sin', 'sio', 'sit', 'sla',
'slo', 'slv', 'sma', 'sme', 'smi', 'smj', 'smn', 'smo', 'sms', 'sna', 'snd', 'snk',
'sog', 'som', 'son', 'sot', 'spa', 'srd', 'srp', 'srr', 'ssa', 'ssw', 'suk', 'sun',
'sus', 'sux', 'swa', 'swe', 'syr', 'tah', 'tai', 'tam', 'tat', 'tel', 'tem', 'ter',
'tet', 'tgk', 'tgl', 'tha', 'tib', 'tig', 'tir', 'tiv', 'tkl', 'tlh', 'tli', 'tmh',
'tog', 'ton', 'tpi', 'tsi', 'tsn', 'tso', 'tuk', 'tum', 'tup', 'tur', 'tut', 'tvl',
'twi', 'tyv', 'udm', 'uga', 'uig', 'ukr', 'umb', 'urd', 'uzb', 'vai', 'ven', 'vie',
'vol', 'vot', 'wak', 'wal', 'war', 'was', 'wel', 'wen', 'wln', 'wol', 'xal', 'xho',
'yao', 'yap', 'yid', 'yor', 'ypk', 'zap', 'zen', 'zha', 'znd', 'zul', 'zun',
'por-BR', 'rum-MD'])
language_map = {'mol': Language('rum-MD'), 'scc': Language('srp'), 'pob': Language('por-BR'),
Language('rum-MD'): 'mol', Language('srp'): 'scc', Language('por-BR'): 'pob'}
language_code = 'alpha3'
videos = [Episode, Movie]
require_video = False
confidence_order = ['moviehash', 'imdbid', 'fulltext']
def __init__(self, config=None):
super(OpenSubtitles, self).__init__(config)
self.server = xmlrpclib.ServerProxy(self.server_url)
self.token = None
def init(self):
super(OpenSubtitles, self).init()
result = self.server.LogIn('', '', 'eng', self.user_agent)
if result['status'] != '200 OK':
raise ServiceError('Login failed')
self.token = result['token']
def terminate(self):
super(OpenSubtitles, self).terminate()
if self.token:
self.server.LogOut(self.token)
def query(self, filepath, languages, moviehash=None, size=None, imdbid=None, query=None):
searches = []
if moviehash and size:
searches.append({'moviehash': moviehash, 'moviebytesize': size})
if imdbid:
searches.append({'imdbid': imdbid})
if query:
searches.append({'query': query})
if not searches:
raise ServiceError('One or more parameter missing')
for search in searches:
search['sublanguageid'] = ','.join(self.get_code(l) for l in languages)
logger.debug(u'Getting subtitles %r with token %s' % (searches, self.token))
results = self.server.SearchSubtitles(self.token, searches)
if not results['data']:
logger.debug(u'Could not find subtitles for %r with token %s' % (searches, self.token))
return []
subtitles = []
for result in results['data']:
language = self.get_language(result['SubLanguageID'])
path = get_subtitle_path(filepath, language, self.config.multi)
confidence = 1 - float(self.confidence_order.index(result['MatchedBy'])) / float(len(self.confidence_order))
subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), result['SubDownloadLink'],
release=to_unicode(result['SubFileName']), confidence=confidence)
subtitles.append(subtitle)
return subtitles
def list_checked(self, video, languages):
results = []
if video.exists:
results = self.query(video.path or video.release, languages, moviehash=video.hashes['OpenSubtitles'], size=str(video.size))
elif video.imdbid:
results = self.query(video.path or video.release, languages, imdbid=video.imdbid)
elif isinstance(video, Episode):
results = self.query(video.path or video.release, languages, query=video.series)
elif isinstance(video, Movie):
results = self.query(video.path or video.release, languages, query=video.title)
return results
def download(self, subtitle):
#TODO: Use OpenSubtitles DownloadSubtitles method
try:
self.download_file(subtitle.link, subtitle.path + '.gz')
with open(subtitle.path, 'wb') as dump:
gz = gzip.open(subtitle.path + '.gz')
dump.write(gz.read())
gz.close()
except Exception as e:
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
raise DownloadFailedError(str(e))
finally:
if os.path.exists(subtitle.path + '.gz'):
os.remove(subtitle.path + '.gz')
return subtitle
Service = OpenSubtitles
-110
View File
@@ -1,110 +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/>.
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, self.__class__.__name__.lower(), result['id'],
release=to_unicode(result['release']), confidence=result['weight'])
subtitles.append(subtitle)
if not subtitles:
return []
# Convert weight to confidence
max_weight = float(max([s.confidence for s in subtitles]))
min_weight = float(min([s.confidence for s in subtitles]))
for subtitle in subtitles:
if max_weight == 0 and min_weight == 0:
subtitle.confidence = 1.0
else:
subtitle.confidence = (subtitle.confidence - min_weight) / (max_weight - min_weight)
return subtitles
def list_checked(self, video, languages):
results = self.query(video.path, languages, video.hashes['OpenSubtitles'])
return results
def download(self, subtitle):
results = self.server.download(self.token, [subtitle.link])
if results['status'] != 200:
raise DownloadFailedError()
subtitle.link = 'http://www.podnapisi.net/static/podnapisi/' + results['names'][0]['filename']
self.download_file(subtitle.link, subtitle.path)
return subtitle
Service = Podnapisi
-101
View File
@@ -1,101 +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/>.
from . import ServiceBase
from ..exceptions import ServiceError
from ..language import language_set, Language
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode, Movie
from bs4 import BeautifulSoup
from subliminal.utils import get_keywords, split_keyword
import logging
import re
import urllib
logger = logging.getLogger(__name__)
class SubsWiki(ServiceBase):
server_url = 'http://www.subswiki.com'
api_based = False
languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'por-BR', 'por', 'spa-ES', u'spa', u'ita', u'cat'])
language_map = {u'Español': Language('spa'), u'Español (España)': Language('spa'), u'Español (Latinoamérica)': Language('spa'),
u'Català': Language('cat'), u'Brazilian': Language('por-BR'), u'English (US)': Language('eng-US'),
u'English (UK)': Language('eng-GB')}
language_code = 'name'
videos = [Episode, Movie]
require_video = False
release_pattern = re.compile('\nVersion (.+), ([0-9]+).([0-9])+ MBs')
required_features = ['permissive']
def list_checked(self, video, languages):
results = []
if isinstance(video, Episode):
results = self.query(video.path or video.release, languages, get_keywords(video.guess), series=video.series, season=video.season, episode=video.episode)
elif isinstance(video, Movie) and video.year:
results = self.query(video.path or video.release, languages, get_keywords(video.guess), movie=video.title, year=video.year)
return results
def query(self, filepath, languages, keywords=None, series=None, season=None, episode=None, movie=None, year=None):
if series and season and episode:
request_series = series.lower().replace(' ', '_')
if isinstance(request_series, unicode):
request_series = request_series.encode('utf-8')
logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
r = self.session.get('%s/serie/%s/%s/%s/' % (self.server_url, urllib.quote(request_series), season, episode))
if r.status_code == 404:
logger.debug(u'Could not find subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
return []
elif movie and year:
request_movie = movie.title().replace(' ', '_')
if isinstance(request_movie, unicode):
request_movie = request_movie.encode('utf-8')
logger.debug(u'Getting subtitles for %s (%d) with languages %r' % (movie, year, languages))
r = self.session.get('%s/film/%s_(%d)' % (self.server_url, urllib.quote(request_movie), year))
if r.status_code == 404:
logger.debug(u'Could not find subtitles for %s (%d) with languages %r' % (movie, year, languages))
return []
else:
raise ServiceError('One or more parameter missing')
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
soup = BeautifulSoup(r.content, self.required_features)
subtitles = []
for sub in soup('td', {'class': 'NewsTitle'}):
sub_keywords = split_keyword(self.release_pattern.search(sub.contents[1]).group(1).lower())
if not keywords & sub_keywords:
logger.debug(u'None of subtitle keywords %r in %r' % (sub_keywords, keywords))
continue
for html_language in sub.parent.parent.findAll('td', {'class': 'language'}):
language = self.get_language(html_language.string.strip())
if language not in languages:
logger.debug(u'Language %r not in wanted languages %r' % (language, languages))
continue
html_status = html_language.findNextSibling('td')
status = html_status.find('strong').string.strip()
if status != 'Completed':
logger.debug(u'Wrong subtitle status %s' % status)
continue
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), '%s%s' % (self.server_url, html_status.findNext('td').find('a')['href']))
subtitles.append(subtitle)
return subtitles
Service = SubsWiki
-88
View File
@@ -1,88 +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/>.
from . import ServiceBase
from ..language import language_set, Language
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode
from bs4 import BeautifulSoup
from subliminal.utils import get_keywords, split_keyword
import logging
import re
import unicodedata
import urllib
logger = logging.getLogger(__name__)
class Subtitulos(ServiceBase):
server_url = 'http://www.subtitulos.es'
api_based = False
languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'por-BR', 'por', 'spa-ES', u'spa', u'ita', u'cat'])
language_map = {u'Español': Language('spa'), u'Español (España)': Language('spa'), u'Español (Latinoamérica)': Language('spa'),
u'Català': Language('cat'), u'Brazilian': Language('por-BR'), u'English (US)': Language('eng-US'),
u'English (UK)': Language('eng-GB'), 'Galego': Language('glg')}
language_code = 'name'
videos = [Episode]
require_video = False
required_features = ['permissive']
# the '.+' in the pattern for Version allows us to match both '&oacute;'
# and the 'ó' char directly. This is because now BS4 converts the html
# code chars into their equivalent unicode char
release_pattern = re.compile('Versi.+n (.+) ([0-9]+).([0-9])+ megabytes')
def list_checked(self, video, languages):
return self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode)
def query(self, filepath, languages, keywords, series, season, episode):
request_series = series.lower().replace(' ', '_')
if isinstance(request_series, unicode):
request_series = unicodedata.normalize('NFKD', request_series).encode('ascii', 'ignore')
logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
r = self.session.get('%s/%s/%sx%.2d' % (self.server_url, urllib.quote(request_series), season, episode))
if r.status_code == 404:
logger.debug(u'Could not find subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
return []
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
soup = BeautifulSoup(r.content, self.required_features)
subtitles = []
for sub in soup('div', {'id': 'version'}):
sub_keywords = split_keyword(self.release_pattern.search(sub.find('p', {'class': 'title-sub'}).contents[1]).group(1).lower())
if not keywords & sub_keywords:
logger.debug(u'None of subtitle keywords %r in %r' % (sub_keywords, keywords))
continue
for html_language in sub.findAllNext('ul', {'class': 'sslist'}):
language = self.get_language(html_language.findNext('li', {'class': 'li-idioma'}).find('strong').contents[0].string.strip())
if language not in languages:
logger.debug(u'Language %r not in wanted languages %r' % (language, languages))
continue
html_status = html_language.findNext('li', {'class': 'li-estado green'})
status = html_status.contents[0].string.strip()
if status != 'Completado':
logger.debug(u'Wrong subtitle status %s' % status)
continue
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), html_status.findNext('span', {'class': 'descargar green'}).find('a')['href'],
keywords=sub_keywords)
subtitles.append(subtitle)
return subtitles
Service = Subtitulos
-63
View File
@@ -1,63 +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/>.
from . import ServiceBase
from ..language import language_set
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode, Movie, UnknownVideo
import logging
logger = logging.getLogger(__name__)
class TheSubDB(ServiceBase):
server_url = 'http://api.thesubdb.com'
user_agent = 'SubDB/1.0 (subliminal/0.6; https://github.com/Diaoul/subliminal)'
api_based = True
# Source: http://api.thesubdb.com/?action=languages
languages = language_set(['af', 'cs', 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'id', 'it',
'la', 'nl', 'no', 'oc', 'pl', 'pt', 'ro', 'ru', 'sl', 'sr', 'sv',
'tr'])
videos = [Movie, Episode, UnknownVideo]
require_video = True
def list_checked(self, video, languages):
return self.query(video.path, video.hashes['TheSubDB'], languages)
def query(self, filepath, moviehash, languages):
r = self.session.get(self.server_url, params={'action': 'search', 'hash': moviehash})
if r.status_code == 404:
logger.debug(u'Could not find subtitles for hash %s' % moviehash)
return []
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
available_languages = language_set(r.content.split(','))
languages &= available_languages
if not languages:
logger.debug(u'Could not find subtitles for hash %s with languages %r (only %r available)' % (moviehash, languages, available_languages))
return []
subtitles = []
for language in languages:
path = get_subtitle_path(filepath, language, self.config.multi)
subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), '%s?action=download&hash=%s&language=%s' % (self.server_url, moviehash, language.alpha2))
subtitles.append(subtitle)
return subtitles
Service = TheSubDB
-142
View File
@@ -1,142 +0,0 @@
# -*- 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'),
'cn': Language('chi')}
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)
return subtitle
Service = TvSubtitles
+156
View File
@@ -0,0 +1,156 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import os.path
import babelfish
import pysrt
from .video import Episode, Movie
logger = logging.getLogger(__name__)
class Subtitle(object):
"""Base class for subtitle
:param language: language of the subtitle
:type language: :class:`babelfish.Language`
:param bool hearing_impaired: `True` if the subtitle is hearing impaired, `False` otherwise
"""
def __init__(self, language, hearing_impaired=False):
self.language = language
self.hearing_impaired = hearing_impaired
def compute_matches(self, video):
"""Compute the matches of the subtitle against the `video`
:param video: the video to compute the matches against
:type video: :class:`~subliminal.video.Video`
:return: matches of the subtitle
:rtype: set
"""
raise NotImplementedError
def compute_score(self, video):
"""Compute the score of the subtitle against the `video`
There are equivalent matches so that a provider can match one element or its equivalent. This is
to give all provider a chance to have a score in the same range without hurting quality.
* Matching :class:`~subliminal.video.Video`'s `hashes` is equivalent to matching everything else
* Matching :class:`~subliminal.video.Episode`'s `season` and `episode`
is equivalent to matching :class:`~subliminal.video.Episode`'s `title`
* Matching :class:`~subliminal.video.Episode`'s `tvdb_id` is equivalent to matching
:class:`~subliminal.video.Episode`'s `series`
:param video: the video to compute the score against
:type video: :class:`~subliminal.video.Video`
:return: score of the subtitle
:rtype: int
"""
score = 0
# compute matches
initial_matches = self.compute_matches(video)
matches = initial_matches.copy()
# hash is the perfect match
if 'hash' in matches:
score = video.scores['hash']
else:
# remove equivalences
if isinstance(video, Episode):
if 'imdb_id' in matches:
matches -= {'series', 'tvdb_id', 'season', 'episode', 'title'}
if 'tvdb_id' in matches:
matches -= {'series'}
if 'title' in matches:
matches -= {'season', 'episode'}
# add other scores
score += sum((video.scores[match] for match in matches))
logger.info('Computed score %d with matches %r', score, initial_matches)
return score
def __repr__(self):
return '<%s [%s]>' % (self.__class__.__name__, self.language)
def get_subtitle_path(video_path, language=None):
"""Create the subtitle path from the given `video_path` and `language`
:param string video_path: path to the video
:param language: language of the subtitle to put in the path
:type language: :class:`babelfish.Language` or None
:return: path of the subtitle
:rtype: string
"""
subtitle_path = os.path.splitext(video_path)[0]
if language is not None:
try:
return subtitle_path + '.%s.%s' % (language.alpha2, 'srt')
except babelfish.ConvertError:
return subtitle_path + '.%s.%s' % (language.alpha3, 'srt')
return subtitle_path + '.srt'
def is_valid_subtitle(subtitle_text):
"""Check if a subtitle text is a valid SubRip format
:return: `True` if the subtitle is valid, `False` otherwise
:rtype: bool
"""
try:
pysrt.from_string(subtitle_text, error_handling=pysrt.ERROR_RAISE)
return True
except pysrt.Error:
pass
except:
logger.exception('Unexpected error when validating subtitle')
return False
def compute_guess_matches(video, guess):
"""Compute matches between a `video` and a `guess`
:param video: the video to compute the matches on
:type video: :class:`~subliminal.video.Video`
:param guess: the guess to compute the matches on
:type guess: :class:`guessit.Guess`
:return: matches of the `guess`
:rtype: set
"""
matches = set()
if isinstance(video, Episode):
# Series
if video.series and 'series' in guess and guess['series'].lower() == video.series.lower():
matches.add('series')
# Season
if video.season and 'seasonNumber' in guess and guess['seasonNumber'] == video.season:
matches.add('season')
# Episode
if video.episode and 'episodeNumber' in guess and guess['episodeNumber'] == video.episode:
matches.add('episode')
elif isinstance(video, Movie):
# Year
if video.year and 'year' in guess and guess['year'] == video.year:
matches.add('year')
# Title
if video.title and 'title' in guess and guess['title'].lower() == video.title.lower():
matches.add('title')
# Release group
if video.release_group and 'releaseGroup' in guess and guess['releaseGroup'].lower() == video.release_group.lower():
matches.add('release_group')
# Screen size
if video.resolution and 'screenSize' in guess and guess['screenSize'] == video.resolution:
matches.add('resolution')
# Video codec
if video.video_codec and 'videoCodec' in guess and guess['videoCodec'] == video.video_codec:
matches.add('video_codec')
# Audio codec
if video.audio_codec and 'audioCodec' in guess and guess['audioCodec'] == video.audio_codec:
matches.add('audio_codec')
return matches
-149
View File
@@ -1,149 +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/>.
from .language import Language
from .utils import to_unicode
import os.path
__all__ = ['Subtitle', 'EmbeddedSubtitle', 'ExternalSubtitle', 'ResultSubtitle', 'get_subtitle_path']
#: Subtitles extensions
EXTENSIONS = ['.srt', '.sub', '.txt']
class Subtitle(object):
"""Base class for subtitles
:param string path: path to the subtitle
:param language: language of the subtitle
:type language: :class:`~subliminal.language.Language`
"""
def __init__(self, path, language):
if not isinstance(language, Language):
raise TypeError('%r is not an instance of Language')
self.path = path
self.language = language
@property
def exists(self):
"""Whether the subtitle exists or not"""
if self.path:
return os.path.exists(self.path)
return False
def __unicode__(self):
return to_unicode(self.path)
def __str__(self):
return unicode(self).encode('utf-8')
def __repr__(self):
return '%s(%s, %s)' % (self.__class__.__name__, self, self.language)
class EmbeddedSubtitle(Subtitle):
"""Subtitle embedded in a container
:param string path: path to the subtitle
:param language: language of the subtitle
:type language: :class:`~subliminal.language.Language`
:param int track_id: id of the subtitle track in the container
"""
def __init__(self, path, language, track_id):
super(EmbeddedSubtitle, self).__init__(path, language)
self.track_id = track_id
@classmethod
def from_enzyme(cls, path, subtitle):
language = Language(subtitle.language, strict=False)
return cls(path, language, subtitle.trackno)
class ExternalSubtitle(Subtitle):
"""Subtitle in a file next to the video file"""
@classmethod
def from_path(cls, path):
"""Create an :class:`ExternalSubtitle` from path"""
extension = None
for e in EXTENSIONS:
if path.endswith(e):
extension = e
break
if extension is None:
raise ValueError('Not a supported subtitle extension')
language = Language(os.path.splitext(path[:len(path) - len(extension)])[1][1:], strict=False)
return cls(path, language)
class ResultSubtitle(ExternalSubtitle):
"""Subtitle found using :mod:`~subliminal.services`
:param string path: path to the subtitle
:param language: language of the subtitle
:type language: :class:`~subliminal.language.Language`
:param string service: name of the service
:param string link: download link for the subtitle
:param string release: release name of the video
:param float confidence: confidence that the subtitle matches the video according to the service
:param set keywords: keywords that describe the subtitle
"""
def __init__(self, path, language, service, link, release=None, confidence=1, keywords=None):
super(ResultSubtitle, self).__init__(path, language)
self.service = service
self.link = link
self.release = release
self.confidence = confidence
self.keywords = keywords or set()
@property
def single(self):
"""Whether this is a single subtitle or not. A single subtitle does not have
a language indicator in its file name
:rtype: bool
"""
return self.language == Language('Undetermined')
def __repr__(self):
if not self.release:
return 'ResultSubtitle(%s, %s, %s, %.2f)' % (self.path, self.language, self.service, self.confidence)
return 'ResultSubtitle(%s, %s, %s, %.2f, release=%s)' % (self.path, self.language, self.service, self.confidence, self.release.encode('ascii', 'ignore'))
def get_subtitle_path(video_path, language, multi):
"""Create the subtitle path from the given video path using language if multi
:param string video_path: path to the video
:param language: language of the subtitle
:type language: :class:`~subliminal.language.Language`
:param bool multi: whether to use multi language naming or not
:return: path of the subtitle
:rtype: string
"""
if not os.path.exists(video_path):
path = os.path.splitext(os.path.basename(video_path))[0]
else:
path = os.path.splitext(video_path)[0]
if multi and language:
return path + '.%s%s' % (language.alpha2, EXTENSIONS[0])
return path + '%s' % EXTENSIONS[0]
-68
View File
@@ -1,68 +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__ = ['Task', 'ListTask', 'DownloadTask', 'StopTask']
class Task(object):
"""Base class for tasks to use in subliminal"""
pass
class ListTask(Task):
"""List task used by the worker to search for subtitles
:param video: video to search subtitles for
:type video: :class:`~subliminal.videos.Video`
:param list languages: languages to search for
:param string service: name of the service to use
:param config: configuration for the service
:type config: :class:`~subliminal.services.ServiceConfig`
"""
def __init__(self, video, languages, service, config):
super(ListTask, self).__init__()
self.video = video
self.service = service
self.languages = languages
self.config = config
def __repr__(self):
return 'ListTask(%r, %r, %s, %r)' % (self.video, self.languages, self.service, self.config)
class DownloadTask(Task):
"""Download task used by the worker to download subtitles
:param video: video to download subtitles for
:type video: :class:`~subliminal.videos.Video`
:param subtitles: subtitles to download in order of preference
:type subtitles: list of :class:`~subliminal.subtitles.Subtitle`
"""
def __init__(self, video, subtitles):
super(DownloadTask, self).__init__()
self.video = video
self.subtitles = subtitles
def __repr__(self):
return 'DownloadTask(%r, %r)' % (self.video, self.subtitles)
class StopTask(Task):
"""Stop task that will stop the worker"""
pass
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from unittest import TextTestRunner, TestSuite
from subliminal import cache_region
from . import test_providers, test_subliminal
cache_region.configure('dogpile.cache.memory', expiration_time=60 * 30)
suite = TestSuite([test_providers.suite(), test_subliminal.suite()])
if __name__ == '__main__':
TextTestRunner().run(suite)
+19
View File
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from subliminal import Movie, Episode
MOVIES = [Movie('Man of Steel (2013)/man.of.steel.2013.720p.bluray.x264-felony.mkv', 'Man of Steel',
release_group='felony', resolution='720p', video_codec='h264', audio_codec='DTS', imdb_id=770828,
size=7033732714, year=2013,
hashes={'opensubtitles': '5b8f8f4e41ccb21e', 'thesubdb': 'ad32876133355929d814457537e12dc2'})]
EPISODES = [Episode('The Big Bang Theory/Season 07/The.Big.Bang.Theory.S07E05.720p.HDTV.X264-DIMENSION.mkv',
'The Big Bang Theory', 7, 5, release_group='DIMENSION', resolution='720p', video_codec='h264',
audio_codec='AC3', imdb_id=3229392, size=501910737, title='The Workplace Proximity',
tvdb_id=80379,
hashes={'opensubtitles': '6878b3ef7c1bd19e', 'thesubdb': '9dbbfb7ba81c9a6237237dae8589fccc'}),
Episode('Game of Thrones/Season 03/Game.of.Thrones.S03E10.Mhysa.720p.WEB-DL.DD5.1.H.264-NTb.mkv',
'Game of Thrones', 3, 10, release_group='NTb', resolution='720p', video_codec='h264',
audio_codec='AC3', imdb_id=2178796, size=2142810931, title='Mhysa', tvdb_id=121361,
hashes={'opensubtitles': 'b850baa096976c22', 'thesubdb': 'b1f899c77f4c960b84b8dbf840d4e42d'})]
+448
View File
@@ -0,0 +1,448 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
from unittest import TestCase, TestSuite, TestLoader, TextTestRunner
from babelfish import Language
from pkg_resources import iter_entry_points
from subliminal import PROVIDERS_ENTRY_POINT
from subliminal.subtitle import is_valid_subtitle
from subliminal.tests.common import MOVIES, EPISODES
class ProviderTestCase(TestCase):
provider_name = ''
def setUp(self):
for provider_entry_point in iter_entry_points(PROVIDERS_ENTRY_POINT, self.provider_name):
self.Provider = provider_entry_point.load()
break
class Addic7edProviderTestCase(ProviderTestCase):
provider_name = 'addic7ed'
def test_find_show_id(self):
with self.Provider() as provider:
show_id = provider.find_show_id('The Big Bang')
self.assertTrue(show_id == 126)
def test_find_show_id_error(self):
with self.Provider() as provider:
show_id = provider.find_show_id('the big how i met your mother')
self.assertTrue(show_id is None)
def test_get_show_ids(self):
with self.Provider() as provider:
show_ids = provider.get_show_ids()
self.assertTrue('the big bang theory' in show_ids and show_ids['the big bang theory'] == 126)
def test_query_episode_0(self):
video = EPISODES[0]
languages = {Language('tur'), Language('rus'), Language('heb'), Language('ita'), Language('fra'),
Language('ron'), Language('nld'), Language('eng'), Language('deu'), Language('ell'),
Language('por', 'BR'), Language('bul')}
matches = {frozenset(['episode', 'release_group', 'title', 'series', 'resolution', 'season']),
frozenset(['series', 'resolution', 'season']),
frozenset(['series', 'episode', 'season', 'title']),
frozenset(['series', 'release_group', 'season']),
frozenset(['series', 'episode', 'season', 'release_group', 'title']),
frozenset(['series', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1(self):
video = EPISODES[1]
languages = {Language('ind'), Language('spa'), Language('hrv'), Language('ita'), Language('fra'),
Language('cat'), Language('ell'), Language('nld'), Language('eng'), Language('fas'),
Language('por'), Language('nor'), Language('deu'), Language('ron'), Language('por', 'BR'),
Language('bul')}
matches = {frozenset(['series', 'episode', 'resolution', 'season', 'title']),
frozenset(['series', 'resolution', 'season']),
frozenset(['series', 'episode', 'season', 'title']),
frozenset(['series', 'release_group', 'season']),
frozenset(['series', 'resolution', 'release_group', 'season']),
frozenset(['series', 'episode', 'season', 'release_group', 'title']),
frozenset(['series', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = EPISODES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['series', 'episode', 'season', 'release_group', 'title']),
frozenset(['series', 'episode', 'season', 'title'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = EPISODES[0]
languages = {Language('eng'), Language('fra')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class BierDopjeProviderTestCase(ProviderTestCase):
provider_name = 'bierdopje'
def test_find_show_id(self):
with self.Provider() as provider:
show_id = provider.find_show_id('The Big Bang')
self.assertTrue(show_id == 9203)
def test_find_show_id_error(self):
with self.Provider() as provider:
show_id = provider.find_show_id('the big how i met your mother')
self.assertTrue(show_id is None)
def test_query_episode_0(self):
video = EPISODES[0]
language = Language('eng')
matches = {frozenset(['series', 'video_codec', 'resolution', 'episode', 'season']),
frozenset(['season', 'video_codec', 'episode', 'series']),
frozenset(['episode', 'video_codec', 'season', 'series', 'resolution', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.query(language, video.season, video.episode, series=video.series)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_query_episode_1(self):
video = EPISODES[1]
language = Language('nld')
matches = {frozenset(['series', 'video_codec', 'resolution', 'episode', 'season']),
frozenset(['season', 'video_codec', 'episode', 'series']),
frozenset(['series', 'episode', 'season']),
frozenset(['season', 'video_codec', 'episode', 'release_group', 'series']),
frozenset(['episode', 'video_codec', 'season', 'series', 'resolution', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.query(language, video.season, video.episode, series=video.series)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_query_episode_0_tvdb_id(self):
video = EPISODES[0]
language = Language('eng')
matches = {frozenset(['video_codec', 'tvdb_id', 'episode', 'season', 'series']),
frozenset(['episode', 'video_codec', 'series', 'season', 'tvdb_id', 'resolution', 'release_group']),
frozenset(['episode', 'series', 'video_codec', 'tvdb_id', 'resolution', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(language, video.season, video.episode, tvdb_id=video.tvdb_id)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_list_subtitles(self):
video = EPISODES[1]
languages = {Language('eng'), Language('nld')}
matches = {frozenset(['series', 'video_codec', 'tvdb_id', 'episode', 'season']),
frozenset(['episode', 'video_codec', 'season', 'series', 'tvdb_id', 'resolution', 'release_group']),
frozenset(['season', 'tvdb_id', 'episode', 'series']),
frozenset(['episode', 'video_codec', 'season', 'series', 'tvdb_id', 'resolution']),
frozenset(['episode', 'video_codec', 'season', 'series', 'tvdb_id', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = EPISODES[0]
languages = {Language('eng'), Language('nld')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class OpenSubtitlesProviderTestCase(ProviderTestCase):
provider_name = 'opensubtitles'
def test_query_movie_0_query(self):
video = MOVIES[0]
languages = {Language('eng')}
matches = {frozenset([]), frozenset(['imdb_id', 'resolution', 'title', 'year']),
frozenset(['imdb_id', 'title', 'year']),
frozenset(['imdb_id', 'video_codec', 'title', 'year']),
frozenset(['imdb_id', 'resolution', 'title', 'video_codec', 'year']),
frozenset(['imdb_id', 'title', 'year', 'video_codec', 'resolution', 'release_group'])}
with self.Provider() as provider:
subtitles = provider.query(languages, query=video.title)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_0_query(self):
video = EPISODES[0]
languages = {Language('eng')}
matches = {frozenset(['series', 'episode', 'season', 'imdb_id']),
frozenset(['series', 'imdb_id', 'video_codec', 'episode', 'season']),
frozenset(['episode', 'title', 'series', 'imdb_id', 'video_codec', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(languages, query=video.name.split(os.sep)[-1])
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1_query(self):
video = EPISODES[1]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['episode', 'title', 'series', 'imdb_id', 'video_codec', 'season']),
frozenset(['series', 'imdb_id', 'title', 'episode', 'season']),
frozenset(['series', 'imdb_id', 'video_codec', 'episode', 'season']),
frozenset(['episode', 'video_codec', 'series', 'imdb_id', 'resolution', 'season']),
frozenset(['series', 'imdb_id', 'resolution', 'episode', 'season']),
frozenset(['series', 'episode', 'season', 'imdb_id'])}
with self.Provider() as provider:
subtitles = provider.query(languages, query=video.name.split(os.sep)[-1])
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_movie_0_imdb_id(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['imdb_id', 'video_codec', 'title', 'year']),
frozenset(['imdb_id', 'resolution', 'title', 'video_codec', 'year']),
frozenset(['imdb_id', 'title', 'year', 'video_codec', 'resolution', 'release_group']),
frozenset(['imdb_id', 'title', 'year']),
frozenset(['imdb_id', 'resolution', 'title', 'year'])}
with self.Provider() as provider:
subtitles = provider.query(languages, imdb_id=video.imdb_id)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_0_imdb_id(self):
video = EPISODES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['series', 'episode', 'season', 'imdb_id']),
frozenset(['episode', 'release_group', 'video_codec', 'series', 'imdb_id', 'resolution', 'season']),
frozenset(['series', 'imdb_id', 'video_codec', 'episode', 'season']),
frozenset(['episode', 'title', 'series', 'imdb_id', 'video_codec', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(languages, imdb_id=video.imdb_id)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_movie_0_hash(self):
video = MOVIES[0]
languages = {Language('eng')}
matches = {frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'imdb_id']),
frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'release_group', 'imdb_id']),
frozenset(['year', 'video_codec', 'imdb_id', 'hash', 'title']),
frozenset(['year', 'resolution', 'imdb_id', 'hash', 'title']),
frozenset(['year', 'imdb_id', 'hash', 'title'])}
with self.Provider() as provider:
subtitles = provider.query(languages, hash=video.hashes['opensubtitles'], size=video.size)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_0_hash(self):
video = EPISODES[0]
languages = {Language('eng')}
matches = {frozenset(['series', 'hash']),
frozenset(['episode', 'season', 'series', 'imdb_id', 'video_codec', 'hash']),
frozenset(['series', 'episode', 'season', 'hash', 'imdb_id']),
frozenset(['series', 'resolution', 'hash', 'video_codec'])}
with self.Provider() as provider:
subtitles = provider.query(languages, hash=video.hashes['opensubtitles'], size=video.size)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['title', 'video_codec', 'year', 'resolution', 'release_group', 'imdb_id']),
frozenset(['imdb_id', 'year', 'title']),
frozenset(['year', 'video_codec', 'imdb_id', 'resolution', 'title']),
frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'release_group', 'imdb_id']),
frozenset(['year', 'video_codec', 'imdb_id', 'hash', 'title']),
frozenset(['year', 'resolution', 'imdb_id', 'hash', 'title']),
frozenset(['hash', 'title', 'video_codec', 'year', 'resolution', 'imdb_id']),
frozenset(['year', 'imdb_id', 'hash', 'title']),
frozenset(['video_codec', 'imdb_id', 'year', 'title']),
frozenset(['year', 'imdb_id', 'resolution', 'title'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class PodnapisiProviderTestCase(ProviderTestCase):
provider_name = 'podnapisi'
def test_query_movie_0(self):
video = MOVIES[0]
language = Language('eng')
matches = {frozenset(['video_codec', 'title', 'resolution', 'year']),
frozenset(['title', 'resolution', 'year']),
frozenset(['video_codec', 'title', 'year']),
frozenset(['title', 'year']),
frozenset(['video_codec', 'title', 'resolution', 'release_group', 'year'])}
with self.Provider() as provider:
subtitles = provider.query(language, title=video.title, year=video.year)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_query_episode_0(self):
video = EPISODES[0]
language = Language('eng')
matches = {frozenset(['episode', 'series', 'season', 'video_codec', 'resolution', 'release_group']),
frozenset(['season', 'video_codec', 'episode', 'resolution', 'series'])}
with self.Provider() as provider:
subtitles = provider.query(language, series=video.series, season=video.season, episode=video.episode)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == {language})
def test_list_subtitles(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['video_codec', 'title', 'resolution', 'year']),
frozenset(['title', 'resolution', 'year']),
frozenset(['video_codec', 'title', 'year']),
frozenset(['title', 'year']),
frozenset(['video_codec', 'title', 'resolution', 'release_group', 'year'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = MOVIES[0]
languages = {Language('eng'), Language('fra')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class TheSubDBProviderTestCase(ProviderTestCase):
provider_name = 'thesubdb'
def test_query_episode_0(self):
video = EPISODES[0]
languages = {Language('eng'), Language('spa'), Language('por')}
matches = {frozenset(['hash'])}
with self.Provider() as provider:
subtitles = provider.query(video.hashes['thesubdb'])
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1(self):
video = EPISODES[1]
languages = {Language('eng'), Language('por')}
matches = {frozenset(['hash'])}
with self.Provider() as provider:
subtitles = provider.query(video.hashes['thesubdb'])
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = MOVIES[0]
languages = {Language('eng'), Language('por')}
matches = {frozenset(['hash'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = MOVIES[0]
languages = {Language('eng'), Language('por')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
class TVsubtitlesProviderTestCase(ProviderTestCase):
provider_name = 'tvsubtitles'
def test_find_show_id(self):
with self.Provider() as provider:
show_id = provider.find_show_id('The Big Bang')
self.assertTrue(show_id == 154)
def test_find_show_id_ambiguous(self):
with self.Provider() as provider:
show_id = provider.find_show_id('New Girl')
self.assertTrue(show_id == 977)
def test_find_show_id_no_dots(self):
with self.Provider() as provider:
show_id = provider.find_show_id('Marvel\'s Agents of S H I E L D')
self.assertTrue(show_id == 1340)
def test_find_show_id_error(self):
with self.Provider() as provider:
show_id = provider.find_show_id('the big gaming')
self.assertTrue(show_id is None)
def test_find_episode_ids(self):
with self.Provider() as provider:
episode_ids = provider.find_episode_ids(154, 5)
self.assertTrue(set(episode_ids.keys()) == set(range(1, 25)))
def test_query_episode_0(self):
video = EPISODES[0]
languages = {Language('fra'), Language('por'), Language('hun'), Language('ron'), Language('eng')}
matches = {frozenset(['series', 'episode', 'season', 'video_codec']),
frozenset(['series', 'episode', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season, video.episode)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_query_episode_1(self):
video = EPISODES[1]
languages = {Language('fra'), Language('ell'), Language('ron'), Language('eng'), Language('hun'),
Language('por'), Language('por', 'BR')}
matches = {frozenset(['series', 'episode', 'resolution', 'season']),
frozenset(['series', 'episode', 'season', 'video_codec']),
frozenset(['series', 'episode', 'season'])}
with self.Provider() as provider:
subtitles = provider.query(video.series, video.season, video.episode)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_list_subtitles(self):
video = EPISODES[0]
languages = {Language('eng'), Language('fra')}
matches = {frozenset(['series', 'episode', 'season'])}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
self.assertTrue({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles} == matches)
self.assertTrue({subtitle.language for subtitle in subtitles} == languages)
def test_download_subtitle(self):
video = EPISODES[0]
languages = {Language('hun')}
with self.Provider() as provider:
subtitles = provider.list_subtitles(video, languages)
subtitle_text = provider.download_subtitle(subtitles[0])
self.assertTrue(is_valid_subtitle(subtitle_text))
def suite():
suite = TestSuite()
suite.addTest(TestLoader().loadTestsFromTestCase(Addic7edProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(BierDopjeProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(OpenSubtitlesProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(PodnapisiProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(TheSubDBProviderTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(TVsubtitlesProviderTestCase))
return suite
if __name__ == '__main__':
TextTestRunner().run(suite())
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import shutil
from unittest import TestCase, TestSuite, TestLoader, TextTestRunner
from babelfish import Language
from subliminal import list_subtitles, download_subtitles, download_best_subtitles, scan_video
from subliminal.tests.common import MOVIES, EPISODES
TEST_DIR = 'test_data'
class ApiTestCase(TestCase):
def setUp(self):
os.mkdir(TEST_DIR)
def tearDown(self):
shutil.rmtree(TEST_DIR)
def test_list_subtitles_movie_0(self):
videos = [MOVIES[0]]
languages = {Language('eng')}
subtitles = list_subtitles(videos, languages)
self.assertTrue(len(subtitles) == len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_list_subtitles_movie_0_por_br(self):
videos = [MOVIES[0]]
languages = {Language('por', 'BR')}
subtitles = list_subtitles(videos, languages)
self.assertTrue(len(subtitles) == len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_list_subtitles_episodes(self):
videos = [EPISODES[0], EPISODES[1]]
languages = {Language('eng'), Language('fra')}
subtitles = list_subtitles(videos, languages)
self.assertTrue(len(subtitles) == len(videos))
self.assertTrue(len(subtitles[videos[0]]) > 0)
def test_download_subtitles(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = list_subtitles(videos, languages)
download_subtitles(subtitles)
for video in videos:
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.en.srt'))
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.fr.srt'))
def test_download_subtitles_single(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = list_subtitles(videos, languages)
download_subtitles(subtitles, single=True)
for video in videos:
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.srt'))
def test_download_best_subtitles(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = download_best_subtitles(videos, languages)
for video in videos:
self.assertTrue(video in subtitles and len(subtitles[video]) == 2)
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.en.srt'))
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.fr.srt'))
def test_download_best_subtitles_single(self):
videos = [EPISODES[0], EPISODES[1]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = download_best_subtitles(videos, languages, single=True)
for video in videos:
self.assertTrue(video in subtitles and len(subtitles[video]) == 1)
self.assertTrue(os.path.exists(os.path.splitext(video.name)[0] + '.srt'))
def test_download_best_subtitles_min_score(self):
videos = [MOVIES[0]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng'), Language('fra')}
subtitles = download_best_subtitles(videos, languages, min_score=1000)
self.assertTrue(len(subtitles) == 0)
def test_download_best_subtitles_hearing_impaired(self):
videos = [MOVIES[0]]
for video in videos:
video.name = os.path.join(TEST_DIR, video.name.split(os.sep)[-1])
languages = {Language('eng')}
subtitles = download_best_subtitles(videos, languages, hearing_impaired=True)
self.assertTrue(subtitles[videos[0]][0].hearing_impaired == True)
class VideoTestCase(TestCase):
def setUp(self):
os.mkdir(TEST_DIR)
for video in MOVIES + EPISODES:
open(os.path.join(TEST_DIR, os.path.split(video.name)[1]), 'w').close()
def tearDown(self):
shutil.rmtree(TEST_DIR)
def test_scan_video_movie(self):
video = MOVIES[0]
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.name == os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.title.lower() == video.title.lower())
self.assertTrue(scanned_video.year == video.year)
self.assertTrue(scanned_video.video_codec == video.video_codec)
self.assertTrue(scanned_video.resolution == video.resolution)
self.assertTrue(scanned_video.release_group == video.release_group)
self.assertTrue(scanned_video.subtitle_languages == set())
self.assertTrue(scanned_video.hashes == {})
self.assertTrue(scanned_video.audio_codec is None)
self.assertTrue(scanned_video.imdb_id is None)
self.assertTrue(scanned_video.size == 0)
def test_scan_video_episode(self):
video = EPISODES[0]
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.name == os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.series == video.series)
self.assertTrue(scanned_video.season == video.season)
self.assertTrue(scanned_video.episode == video.episode)
self.assertTrue(scanned_video.video_codec == video.video_codec)
self.assertTrue(scanned_video.resolution == video.resolution)
self.assertTrue(scanned_video.release_group == video.release_group)
self.assertTrue(scanned_video.subtitle_languages == set())
self.assertTrue(scanned_video.hashes == {})
self.assertTrue(scanned_video.title is None)
self.assertTrue(scanned_video.tvdb_id is None)
self.assertTrue(scanned_video.imdb_id is None)
self.assertTrue(scanned_video.audio_codec is None)
self.assertTrue(scanned_video.size == 0)
def test_scan_video_subtitle_language_und(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.subtitle_languages == {Language('und')})
def test_scan_video_subtitles_language_eng(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.en.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.subtitle_languages == {Language('eng')})
def test_scan_video_subtitles_languages(self):
video = EPISODES[0]
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.en.srt', 'w').close()
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.fr.srt', 'w').close()
open(os.path.join(TEST_DIR, os.path.splitext(os.path.split(video.name)[1])[0]) + '.srt', 'w').close()
scanned_video = scan_video(os.path.join(TEST_DIR, os.path.split(video.name)[1]))
self.assertTrue(scanned_video.subtitle_languages == {Language('eng'), Language('fra'), Language('und')})
def suite():
suite = TestSuite()
suite.addTest(TestLoader().loadTestsFromTestCase(ApiTestCase))
suite.addTest(TestLoader().loadTestsFromTestCase(VideoTestCase))
return suite
if __name__ == '__main__':
TextTestRunner().run(suite())
-69
View File
@@ -1,69 +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/>.
import re
__all__ = ['get_keywords', 'split_keyword', 'to_unicode']
def get_keywords(guess):
"""Retrieve keywords from guessed informations
:param guess: guessed informations
:type guess: :class:`guessit.guess.Guess`
:return: lower case alphanumeric keywords
:rtype: set
"""
keywords = set()
for k in ['releaseGroup', 'screenSize', 'videoCodec', 'format']:
if k in guess:
keywords = keywords | split_keyword(guess[k].lower())
return keywords
def split_keyword(keyword):
"""Split a keyword in multiple ones on any non-alphanumeric character
:param string keyword: keyword
:return: keywords
:rtype: set
"""
split = set(re.findall(r'\w+', keyword))
return split
def to_unicode(data):
"""Convert a basestring to unicode
:param basestring data: data to decode
:return: data as unicode
:rtype: unicode
"""
if not isinstance(data, basestring):
raise ValueError('Basestring expected')
if isinstance(data, unicode):
return data
for encoding in ('utf-8', 'latin-1'):
try:
return unicode(data, encoding)
except UnicodeDecodeError:
pass
return unicode(data, 'utf-8', 'replace')
+371
View File
@@ -0,0 +1,371 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import datetime
import hashlib
import logging
import os
import struct
import babelfish
import enzyme
import guessit
logger = logging.getLogger(__name__)
#: Video extensions
VIDEO_EXTENSIONS = ('.3g2', '.3gp', '.3gp2', '.3gpp', '.60d', '.ajp', '.asf', '.asx', '.avchd', '.avi', '.bik',
'.bix', '.box', '.cam', '.dat', '.divx', '.dmf', '.dv', '.dvr-ms', '.evo', '.flc', '.fli',
'.flic', '.flv', '.flx', '.gvi', '.gvp', '.h264', '.m1v', '.m2p', '.m2ts', '.m2v', '.m4e',
'.m4v', '.mjp', '.mjpeg', '.mjpg', '.mkv', '.moov', '.mov', '.movhd', '.movie', '.movx', '.mp4',
'.mpe', '.mpeg', '.mpg', '.mpv', '.mpv2', '.mxf', '.nsv', '.nut', '.ogg', '.ogm', '.omf', '.ps',
'.qt', '.ram', '.rm', '.rmvb', '.swf', '.ts', '.vfw', '.vid', '.video', '.viv', '.vivo', '.vob',
'.vro', '.wm', '.wmv', '.wmx', '.wrap', '.wvx', '.wx', '.x264', '.xvid')
#: Subtitle extensions
SUBTITLE_EXTENSIONS = ('.srt', '.sub', '.smi', '.txt', '.ssa', '.ass', '.mpl')
class Video(object):
"""Base class for videos
Represent a video, existing or not, with various properties that defines it.
Each property has an associated score based on equations that are described in
subclasses.
:param string name: name or path of the video
:param string release_group: release group of the video
:param string resolution: screen size of the video stream (480p, 720p, 1080p or 1080i)
:param string video_codec: codec of the video stream
:param string audio_codec: codec of the main audio stream
:param int imdb_id: IMDb id of the video
:param dict hashes: hashes of the video file by provider names
:param int size: byte size of the video file
:param set subtitle_languages: existing subtitle languages
"""
scores = {}
def __init__(self, name, release_group=None, resolution=None, video_codec=None, audio_codec=None, imdb_id=None,
hashes=None, size=None, subtitle_languages=None):
self.name = name
self.release_group = release_group
self.resolution = resolution
self.video_codec = video_codec
self.audio_codec = audio_codec
self.imdb_id = imdb_id
self.hashes = hashes or {}
self.size = size
self.subtitle_languages = subtitle_languages or set()
@classmethod
def fromguess(cls, name, guess):
if guess['type'] == 'episode':
return Episode.fromguess(name, guess)
if guess['type'] == 'movie':
return Movie.fromguess(name, guess)
raise ValueError('The guess must be an episode or a movie guess')
def __repr__(self):
return '<%s [%r]>' % (self.__class__.__name__, self.name)
def __hash__(self):
return hash(self.name)
class Episode(Video):
"""Episode :class:`Video`
Scores are defined by a set of equations, see :func:`~subliminal.score.get_episode_equations`
:param string series: series of the episode
:param int season: season number of the episode
:param int episode: episode number of the episode
:param string title: title of the episode
:param int tvdb_id: TheTVDB id of the episode
"""
scores = {'title': 12, 'video_codec': 2, 'imdb_id': 35, 'audio_codec': 1, 'tvdb_id': 23, 'resolution': 2,
'season': 6, 'release_group': 6, 'series': 23, 'episode': 6, 'hash': 46}
def __init__(self, name, series, season, episode, release_group=None, resolution=None, video_codec=None,
audio_codec=None, imdb_id=None, hashes=None, size=None, subtitle_languages=None, title=None,
tvdb_id=None):
super(Episode, self).__init__(name, release_group, resolution, video_codec, audio_codec, imdb_id, hashes,
size, subtitle_languages)
self.series = series
self.season = season
self.episode = episode
self.title = title
self.tvdb_id = tvdb_id
@classmethod
def fromguess(cls, name, guess):
if guess['type'] != 'episode':
raise ValueError('The guess must be an episode guess')
if 'series' not in guess or 'season' not in guess or 'episodeNumber' not in guess:
raise ValueError('Insufficient data to process the guess')
return cls(name, guess['series'], guess['season'], guess['episodeNumber'],
release_group=guess.get('releaseGroup'), resolution=guess.get('screenSize'),
video_codec=guess.get('videoCodec'), audio_codec=guess.get('audioCodec'),
title=guess.get('title'))
def __repr__(self):
return '<%s [%r, %rx%r]>' % (self.__class__.__name__, self.series, self.season, self.episode)
class Movie(Video):
"""Movie :class:`Video`
Scores are defined by a set of equations, see :func:`~subliminal.score.get_movie_equations`
:param string title: title of the movie
:param int year: year of the movie
"""
scores = {'title': 13, 'video_codec': 2, 'resolution': 2, 'audio_codec': 1, 'year': 7, 'imdb_id': 31,
'release_group': 6, 'hash': 31}
def __init__(self, name, title, release_group=None, resolution=None, video_codec=None, audio_codec=None,
imdb_id=None, hashes=None, size=None, subtitle_languages=None, year=None):
super(Movie, self).__init__(name, release_group, resolution, video_codec, audio_codec, imdb_id, hashes,
size, subtitle_languages)
self.title = title
self.year = year
@classmethod
def fromguess(cls, name, guess):
if guess['type'] != 'movie':
raise ValueError('The guess must be a movie guess')
if 'title' not in guess:
raise ValueError('Insufficient data to process the guess')
return cls(name, guess['title'], release_group=guess.get('releaseGroup'), resolution=guess.get('screenSize'),
video_codec=guess.get('videoCodec'), audio_codec=guess.get('audioCodec'),
year=guess.get('year'))
def __repr__(self):
if self.year is None:
return '<%s [%r]>' % (self.__class__.__name__, self.title)
return '<%s [%r, %r]>' % (self.__class__.__name__, self.title, self.year)
def scan_subtitle_languages(path):
"""Search for subtitles with alpha2 extension from a video `path` and return their language
:param string path: path to the video
:return: found subtitle languages
:rtype: set
"""
language_extensions = tuple('.' + c for c in babelfish.CONVERTERS['alpha2'].codes)
dirpath, filename = os.path.split(path)
subtitles = set()
for p in os.listdir(dirpath):
if not isinstance(p, bytes) and p.startswith(os.path.splitext(filename)[0]) and p.endswith(SUBTITLE_EXTENSIONS):
if os.path.splitext(p)[0].endswith(language_extensions):
subtitles.add(babelfish.Language.fromalpha2(os.path.splitext(p)[0][-2:]))
else:
subtitles.add(babelfish.Language('und'))
logger.debug('Found subtitles %r', subtitles)
return subtitles
def scan_video(path, subtitles=True, embedded_subtitles=True):
"""Scan a video and its subtitle languages from a video `path`
:param string path: absolute path to the video
:param bool subtitles: scan for subtitles with the same name
:param bool embedded_subtitles: scan for embedded subtitles
:return: the scanned video
:rtype: :class:`Video`
:raise: ValueError if cannot guess enough information from the path
"""
dirpath, filename = os.path.split(path)
logger.info('Scanning video %r in %r', filename, dirpath)
video = Video.fromguess(path, guessit.guess_file_info(path, 'autodetect'))
video.size = os.path.getsize(path)
if video.size > 10485760:
logger.debug('Size is %d', video.size)
video.hashes['opensubtitles'] = hash_opensubtitles(path)
video.hashes['thesubdb'] = hash_thesubdb(path)
logger.debug('Computed hashes %r', video.hashes)
else:
logger.warning('Size is lower than 10MB: hashes not computed')
if subtitles:
video.subtitle_languages |= scan_subtitle_languages(path)
# enzyme
try:
if filename.endswith('.mkv'):
with open(path, 'rb') as f:
mkv = enzyme.MKV(f)
if mkv.video_tracks:
video_track = mkv.video_tracks[0]
# resolution
if video_track.height in (480, 720, 1080):
if video_track.interlaced:
video.resolution = '%di' % video_track.height
logger.debug('Found resolution %s with enzyme', video.resolution)
else:
video.resolution = '%dp' % video_track.height
logger.debug('Found resolution %s with enzyme', video.resolution)
# video codec
if video_track.codec_id == 'V_MPEG4/ISO/AVC':
video.video_codec = 'h264'
logger.debug('Found video_codec %s with enzyme', video.video_codec)
elif video_track.codec_id == 'V_MPEG4/ISO/SP':
video.video_codec = 'DivX'
logger.debug('Found video_codec %s with enzyme', video.video_codec)
elif video_track.codec_id == 'V_MPEG4/ISO/ASP':
video.video_codec = 'XviD'
logger.debug('Found video_codec %s with enzyme', video.video_codec)
else:
logger.warning('MKV has no video track')
if mkv.audio_tracks:
audio_track = mkv.audio_tracks[0]
# audio codec
if audio_track.codec_id == 'A_AC3':
video.audio_codec = 'AC3'
logger.debug('Found audio_codec %s with enzyme', video.audio_codec)
elif audio_track.codec_id == 'A_DTS':
video.audio_codec = 'DTS'
logger.debug('Found audio_codec %s with enzyme', video.audio_codec)
elif audio_track.codec_id == 'A_AAC':
video.audio_codec = 'AAC'
logger.debug('Found audio_codec %s with enzyme', video.audio_codec)
else:
logger.warning('MKV has no audio track')
if mkv.subtitle_tracks:
# embedded subtitles
if embedded_subtitles:
embedded_subtitle_languages = set()
for st in mkv.subtitle_tracks:
try:
embedded_subtitle_languages.add(babelfish.Language.fromalpha3b(st.language or 'und'))
except babelfish.Error:
logger.error('Embedded subtitle language %r is not a valid language', st.language)
logger.debug('Found embedded subtitle %r with enzyme', embedded_subtitle_languages)
video.subtitle_languages |= embedded_subtitle_languages
else:
logger.debug('MKV has no subtitle track')
except enzyme.Error:
logger.error('Parsing video metadata with enzyme failed')
return video
def scan_videos(paths, subtitles=True, embedded_subtitles=True, age=None):
"""Scan `paths` for videos and their subtitle languages
:params paths: absolute paths to scan for videos
:type paths: list of string
:param bool subtitles: scan for subtitles with the same name
:param bool embedded_subtitles: scan for embedded subtitles
:param age: age of the video, if any
:type age: datetime.timedelta or None
:return: the scanned videos
:rtype: list of :class:`Video`
"""
videos = []
# scan files
for filepath in [p for p in paths if os.path.isfile(p)]:
if age and datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(filepath)) > age:
logger.info('Skipping video %r: older than %r', filepath, age)
continue
try:
videos.append(scan_video(filepath, subtitles, embedded_subtitles))
except ValueError as e:
logger.error('Skipping video: %s', e)
continue
# scan directories
for path in [p for p in paths if os.path.isdir(p)]:
logger.info('Scanning directory %r', path)
for dirpath, dirnames, filenames in os.walk(path):
# skip badly encoded directories
if isinstance(dirpath, bytes):
logger.error('Skipping badly encoded directory %r', dirpath.decode('utf-8', errors='replace'))
continue
# skip badly encoded and hidden sub directories
for dirname in list(dirnames):
if isinstance(dirname, bytes):
logger.error('Skipping badly encoded dirname %r in %r', dirname.decode('utf-8', errors='replace'),
dirpath)
dirnames.remove(dirname)
elif dirname.startswith('.'):
logger.debug('Skipping hidden dirname %r in %r', dirname, dirpath)
dirnames.remove(dirname)
# scan for videos
for filename in filenames:
# skip badly encoded files
if isinstance(filename, bytes):
logger.error('Skipping badly encoded filename %r in %r', filename.decode('utf-8', errors='replace'),
dirpath)
continue
# filter videos
if not filename.endswith(VIDEO_EXTENSIONS):
continue
# skip hidden files
if filename.startswith('.'):
logger.debug('Skipping hidden filename %r in %r', filename, dirpath)
continue
filepath = os.path.join(dirpath, filename)
# skip links
if os.path.islink(filepath):
logger.debug('Skipping link %r in %r', filename, dirpath)
continue
if age and datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(filepath)) > age:
logger.info('Skipping video %r: older than %r', filepath, age)
continue
try:
video = scan_video(filepath, subtitles, embedded_subtitles)
except ValueError as e:
logger.error('Skipping video: %s', e)
continue
videos.append(video)
return videos
def hash_opensubtitles(video_path):
"""Compute a hash using OpenSubtitles' algorithm
:param string video_path: path of the video
:return: the hash
:rtype: string
"""
bytesize = struct.calcsize(b'q')
with open(video_path, 'rb') as f:
filesize = os.path.getsize(video_path)
filehash = filesize
if filesize < 65536 * 2:
return None
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(b'q', filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF # to remain as 64bit number
f.seek(max(0, filesize - 65536), 0)
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(b'q', filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF
returnedhash = '%016x' % filehash
return returnedhash
def hash_thesubdb(video_path):
"""Compute a hash using TheSubDB's algorithm
:param string video_path: path of the video
:return: the hash
:rtype: string
"""
readsize = 64 * 1024
if os.path.getsize(video_path) < readsize:
return None
with open(video_path, 'rb') as f:
data = f.read(readsize)
f.seek(-readsize, os.SEEK_END)
data += f.read(readsize)
return hashlib.md5(data).hexdigest().decode('ascii')
-284
View File
@@ -1,284 +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/>.
from . import subtitles
from .language import Language
from .utils import to_unicode
import enzyme
import guessit
import hashlib
import logging
import mimetypes
import os
import struct
__all__ = ['EXTENSIONS', 'MIMETYPES', 'Video', 'Episode', 'Movie', 'UnknownVideo',
'scan', 'hash_opensubtitles', 'hash_thesubdb']
logger = logging.getLogger(__name__)
#: Video extensions
EXTENSIONS = ['.avi', '.mkv', '.mpg', '.mp4', '.m4v', '.mov', '.ogm', '.ogv', '.wmv',
'.divx', '.asf']
#: Video mimetypes
MIMETYPES = ['video/mpeg', 'video/mp4', 'video/quicktime', 'video/x-ms-wmv', 'video/x-msvideo',
'video/x-flv', 'video/x-matroska', 'video/x-matroska-3d']
class Video(object):
"""Base class for videos
:param string path: path
:param guess: guessed informations
:type guess: :class:`~guessit.guess.Guess`
:param string imdbid: imdbid
"""
def __init__(self, path, guess, imdbid=None):
self.release = path
self.guess = guess
self.imdbid = imdbid
self._path = None
self.hashes = {}
if os.path.exists(path):
self._path = path
self.size = os.path.getsize(self._path)
self._compute_hashes()
@classmethod
def from_path(cls, path):
"""Create a :class:`Video` subclass guessing all informations from the given path
:param string path: path
:return: video object
:rtype: :class:`Episode` or :class:`Movie` or :class:`UnknownVideo`
"""
guess = guessit.guess_file_info(path, 'autodetect')
result = None
if guess['type'] == 'episode' and 'series' in guess and 'season' in guess and 'episodeNumber' in guess:
title = None
if 'title' in guess:
title = guess['title']
result = Episode(path, guess['series'], guess['season'], guess['episodeNumber'], title, guess)
if guess['type'] == 'movie' and 'title' in guess:
year = None
if 'year' in guess:
year = guess['year']
result = Movie(path, guess['title'], year, guess)
if not result:
result = UnknownVideo(path, guess)
if not isinstance(result, cls):
raise ValueError('Video is not of requested type')
return result
@property
def exists(self):
"""Whether the video exists or not"""
if self._path:
return os.path.exists(self._path)
return False
@property
def path(self):
"""Path to the video"""
return self._path
@path.setter
def path(self, value):
if not os.path.exists(value):
raise ValueError('Path does not exists')
self._path = value
self.size = os.path.getsize(self._path)
self._compute_hashes()
def _compute_hashes(self):
"""Compute different hashes"""
self.hashes['OpenSubtitles'] = hash_opensubtitles(self.path)
self.hashes['TheSubDB'] = hash_thesubdb(self.path)
def scan(self):
"""Scan and return associated subtitles
:return: associated subtitles
:rtype: list of :class:`~subliminal.subtitles.Subtitle`
"""
if not self.exists:
return []
basepath = os.path.splitext(self.path)[0]
results = []
video_infos = None
try:
video_infos = enzyme.parse(self.path)
logger.debug(u'Succeeded parsing %s with enzyme: %r' % (self.path, video_infos))
except:
logger.debug(u'Failed parsing %s with enzyme' % self.path)
if isinstance(video_infos, enzyme.core.AVContainer):
results.extend([subtitles.EmbeddedSubtitle.from_enzyme(self.path, s) for s in video_infos.subtitles])
# cannot use glob here because it chokes if there are any square
# brackets inside the filename, so we have to use basic string
# startswith/endswith comparisons
folder, basename = os.path.split(basepath)
existing = [f for f in os.listdir(folder) if f.startswith(basename)]
for path in existing:
for ext in subtitles.EXTENSIONS:
if path.endswith(ext):
language = Language(path[len(basename) + 1:-len(ext)], strict=False)
results.append(subtitles.ExternalSubtitle(path, language))
return results
def __unicode__(self):
return to_unicode(self.path or self.release)
def __str__(self):
return unicode(self).encode('utf-8')
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__, self)
def __hash__(self):
return hash(self.path or self.release)
class Episode(Video):
"""Episode :class:`Video`
:param string path: path
:param string series: series
:param int season: season number
:param int episode: episode number
:param string title: title
:param guess: guessed informations
:type guess: :class:`~guessit.guess.Guess`
:param string tvdbid: tvdbid
:param string imdbid: imdbid
"""
def __init__(self, path, series, season, episode, title=None, guess=None, tvdbid=None, imdbid=None):
super(Episode, self).__init__(path, guess, imdbid)
self.series = series
self.title = title
self.season = season
self.episode = episode
self.tvdbid = tvdbid
class Movie(Video):
"""Movie :class:`Video`
:param string path: path
:param string title: title
:param int year: year
:param guess: guessed informations
:type guess: :class:`~guessit.guess.Guess`
:param string imdbid: imdbid
"""
def __init__(self, path, title, year=None, guess=None, imdbid=None):
super(Movie, self).__init__(path, guess, imdbid)
self.title = title
self.year = year
class UnknownVideo(Video):
"""Unknown video"""
pass
def scan(entry, max_depth=3, scan_filter=None, depth=0):
"""Scan a path for videos and subtitles
:param string entry: path
:param int max_depth: maximum folder depth
:param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
:param int depth: starting depth
:return: found videos and subtitles
:rtype: list of (:class:`Video`, [:class:`~subliminal.subtitles.Subtitle`])
"""
if depth > max_depth and max_depth != 0: # we do not want to search the whole file system except if max_depth = 0
return []
if os.path.isdir(entry): # a dir? recurse
logger.debug(u'Scanning directory %s with depth %d/%d' % (entry, depth, max_depth))
result = []
for e in os.listdir(entry):
result.extend(scan(os.path.join(entry, e), max_depth, scan_filter, depth + 1))
return result
if os.path.isfile(entry) or depth == 0:
logger.debug(u'Scanning file %s with depth %d/%d' % (entry, depth, max_depth))
if depth != 0: # trust the user: only check for valid format if recursing
if mimetypes.guess_type(entry)[0] not in MIMETYPES and os.path.splitext(entry)[1] not in EXTENSIONS:
return []
if scan_filter is not None and scan_filter(entry):
return []
video = Video.from_path(entry)
return [(video, video.scan())]
logger.warning(u'Scanning entry %s failed with depth %d/%d' % (entry, depth, max_depth))
return [] # anything else
def hash_opensubtitles(path):
"""Compute a hash using OpenSubtitles' algorithm
:param string path: path
:return: hash
:rtype: string
"""
longlongformat = 'q' # long long
bytesize = struct.calcsize(longlongformat)
with open(path, 'rb') as f:
filesize = os.path.getsize(path)
filehash = filesize
if filesize < 65536 * 2:
return None
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(longlongformat, filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF # to remain as 64bit number
f.seek(max(0, filesize - 65536), 0)
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(longlongformat, filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF
returnedhash = '%016x' % filehash
logger.debug(u'Computed OpenSubtitle hash %s for %s' % (returnedhash, path))
return returnedhash
def hash_thesubdb(path):
"""Compute a hash using TheSubDB's algorithm
:param string path: path
:return: hash
:rtype: string
"""
readsize = 64 * 1024
if os.path.getsize(path) < readsize:
return None
with open(path, 'rb') as f:
data = f.read(readsize)
f.seek(-readsize, os.SEEK_END)
data += f.read(readsize)
returnedhash = hashlib.md5(data).hexdigest()
logger.debug(u'Computed TheSubDB hash %s for %s' % (returnedhash, path))
return returnedhash
-26
View File
@@ -1,26 +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/>.
from . import test_language, test_services, test_subliminal, test_videos
import unittest
suite = unittest.TestSuite([test_language.suite(), test_services.suite(), test_subliminal.suite(), test_videos.suite()])
if __name__ == '__main__':
unittest.TextTestRunner().run(suite)
-163
View File
@@ -1,163 +0,0 @@
#!/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())
-501
View File
@@ -1,501 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from subliminal import videos
from subliminal.exceptions import ServiceError
from subliminal.language import language_set, LANGUAGES
from subliminal.services import ServiceConfig
from subliminal.services.addic7ed import Addic7ed
from subliminal.services.bierdopje import BierDopje
from subliminal.services.opensubtitles import OpenSubtitles
from subliminal.services.podnapisi import Podnapisi
from subliminal.services.subswiki import SubsWiki
from subliminal.services.subtitulos import Subtitulos
from subliminal.services.thesubdb import TheSubDB
from subliminal.services.tvsubtitles import TvSubtitles
import os
import unittest
try:
import cPickle as pickle
except ImportError:
import pickle
cache_dir = u'/tmp/sublicache'
if not os.path.exists(cache_dir):
os.mkdir(cache_dir)
existing_video = u'/something/The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4'
class ServiceTestCase(unittest.TestCase):
def setUp(self):
self.wrong_languages = language_set(list(language_set(LANGUAGES) - self.service.languages)[:2])
def tearDown(self):
# Setting config to None allows to delete the object, which will in turn save the cache
self.config = None
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(service, self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(service, self.fake_file, self.languages, self.episode_keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(service, self.fake_file, self.wrong_languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_list_episode(self):
video = videos.Video.from_path(self.episode_path)
with self.service(self.config) as service:
results = service.list(video, self.languages)
self.assertTrue(len(results) > 0)
def test_list_movie(self):
video = videos.Video.from_path(self.movie_path)
with self.service(self.config) as service:
results = service.list(video, self.languages)
self.assertTrue(len(results) > 0)
def test_list_wrong_languages(self):
video = videos.Video.from_path(self.episode_path)
with self.service(self.config) as service:
results = service.list(video, self.wrong_languages)
self.assertTrue(len(results) == 0)
def test_download_episode(self):
video = videos.Video.from_path(self.episode_path)
with self.service(self.config) as service:
subtitle = service.list(video, language_set([self.episode_sublanguage]))[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
service.download(subtitle)
self.assertTrue(os.path.exists(subtitle.path))
self.assertTrue(os.path.getsize(subtitle.path) in self.episode_subfilesizes, msg='Size %d not in %r' % (os.path.getsize(subtitle.path), self.episode_subfilesizes))
os.remove(subtitle.path)
def test_download_movie(self):
video = videos.Video.from_path(self.movie_path)
with self.service(self.config) as service:
subtitle = service.list(video, language_set([self.movie_sublanguage]))[0]
if os.path.exists(subtitle.path):
os.remove(subtitle.path)
service.download(subtitle)
self.assertTrue(os.path.exists(subtitle.path))
self.assertTrue(os.path.getsize(subtitle.path) in self.movie_subfilesizes, msg='Size %d not in %r' % (os.path.getsize(subtitle.path), self.movie_subfilesizes))
os.remove(subtitle.path)
def test_cached_series(self):
with self.service(self.config) as service:
service.clear_cache()
service.query(self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
service.save_cache()
c = pickle.load(open(os.path.join(cache_dir, 'subliminal_%s.cache' % self.service.__name__)))
found = False
for _, cached_values in c.items():
for args, __ in cached_values.items():
if args == (self.series.lower(),):
found = True
self.assertTrue(found)
class Addic7edTestCase(ServiceTestCase):
query_tests = ['test_query_series', 'test_query_wrong_series', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = ['test_cached_series']
service = Addic7ed
def setUp(self):
super(Addic7edTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.fake_file = u'/tmp/fake_file'
self.languages = language_set(['en', 'fr'])
self.episode_path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.episode_sublanguage = 'en'
# FIXME: this is the size of the first subtitle that appears on the page
# which is the original one, not the most updated one. We should make
# sure the Addic7ed service picks up the most recent one instead
self.episode_subfilesizes = [33538, 33643]
self.episode_keywords = set(['asap', 'hdtv'])
self.series = 'The Big Bang Theory'
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
class BierDopjeTestCase(ServiceTestCase):
query_tests = ['test_query_series', 'test_query_wrong_series', 'test_query_wrong_languages',
'test_query_tvdbid', 'test_query_wrong_tvdbid', 'test_query_series_and_tvdbid']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = ['test_cached_series']
service = BierDopje
def setUp(self):
super(BierDopjeTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.episode_path = u'The Big Bang Theory/Season 05/S05E06 - The Rhinitis Revelation - HD TV.mkv'
self.episode_sublanguage = 'en'
self.episode_subfilesizes = [33469]
self.movie_path = u'Inception (2010)/Inception - 1080p.mkv'
self.movie_sublanguage = 'en'
self.movie_subfilesizes = []
self.languages = language_set(['en', 'nl'])
self.fake_file = u'/tmp/fake_file'
self.series = 'The Big Bang Theory'
self.episode_keywords = set()
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
self.tvdbid = 80379
self.wrong_tvdbid = 9999999999
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.season, self.episode, self.languages, series=self.series)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.season, self.episode, self.languages, series=self.wrong_series)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.season, self.episode, self.wrong_languages, series=self.series)
self.assertTrue(len(results) == 0)
def test_query_tvdbid(self):
with BierDopje(self.config) as service:
results = service.query(self.fake_file, self.season, self.episode, self.languages, tvdbid=self.tvdbid)
self.assertTrue(len(results) > 0)
def test_query_series_and_tvdbid(self):
with BierDopje(self.config) as service:
results = service.query(self.fake_file, self.season, self.episode, self.languages, series=self.series, tvdbid=self.tvdbid)
self.assertTrue(len(results) > 0)
def test_query_wrong_tvdbid(self):
with BierDopje(self.config) as service:
results = service.query(self.fake_file, self.season, self.episode, self.languages, tvdbid=self.wrong_tvdbid)
self.assertTrue(len(results) == 0)
def test_cached_series(self):
with self.service(self.config) as service:
service.clear_cache()
service.query(self.fake_file, self.season, self.episode, self.languages, series=self.series)
service.save_cache()
c = pickle.load(open(os.path.join(cache_dir, 'subliminal_%s.cache' % self.service.__name__)))
found = False
for _, cached_values in c.items():
for args, __ in cached_values.items():
if args == (self.series.lower(),):
found = True
self.assertTrue(found)
class OpenSubtitlesTestCase(ServiceTestCase):
query_tests = ['test_query_query', 'test_query_imdbid', 'test_query_hash', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = []
service = OpenSubtitles
def setUp(self):
super(OpenSubtitlesTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.languages = language_set(['en', 'fr'])
self.fake_file = u'/tmp/fake_file'
self.episode_path = existing_video
self.episode_sublanguage = 'en'
self.episode_subfilesizes = [30374, 30358, 33585, 33547, 33563, 33601]
self.movie = 'Inception'
self.imdbid = '1375666'
self.wrong_imdbid = '9999999'
self.hash = '51e57c4e8fd77990'
self.size = 882571264L
def test_query_query(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, query=self.movie)
self.assertTrue(len(results) > 0)
def test_query_imdbid(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, imdbid=self.imdbid)
self.assertTrue(len(results) > 0)
def test_query_hash(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, moviehash=self.hash, size=self.size)
self.assertTrue(len(results) > 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, moviehash=self.hash, size=self.size)
self.assertTrue(len(results) == 0)
class PodnapisiTestCase(ServiceTestCase):
query_tests = ['test_query', 'test_query_wrong_languages']
list_tests = [] #'test_list', 'test_list_wrong_languages'
download_tests = [] #'test_download'
cache_tests = []
service = Podnapisi
def setUp(self):
super(PodnapisiTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.languages = language_set(['en', 'fr'])
self.fake_file = u'/tmp/fake_file'
self.path = existing_video
self.hash = 'e1b45885346cfa0b'
def test_query(self):
with Podnapisi(self.config) as service:
results = service.query(self.fake_file, self.languages, moviehash=self.hash)
self.assertTrue(len(results) > 0)
def test_query_wrong_languages(self):
with Podnapisi(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, moviehash=self.hash)
self.assertTrue(len(results) == 0)
class SubsWikiTestCase(ServiceTestCase):
query_tests = ['test_query_series', 'test_query_movie', 'test_query_wrong_parameters', 'test_query_wrong_series', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_movie', 'test_list_wrong_languages']
download_tests = ['test_download_episode', 'test_download_movie']
cache_tests = []
service = SubsWiki
def setUp(self):
super(SubsWikiTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.fake_file = u'/tmp/fake_file'
self.languages = language_set(['en', 'es'])
self.movie_path = u'Soul Surfer (2011)/Soul.Surfer.(2011).DVDRip.XviD-TWiZTED.mkv'
self.movie_sublanguage = 'es'
self.movie_subfilesizes = [87528]
self.movie_keywords = set(['twizted'])
self.movie = u'Soul Surfer'
self.movie_year = 2011
self.episode_path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.episode_sublanguage = 'es'
self.episode_subfilesizes = [34040]
self.episode_keywords = set(['asap', 'hdtv'])
self.series = 'The Big Bang Theory'
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_movie(self):
with SubsWiki(self.config) as service:
results = service.query(self.fake_file, self.languages, keywords=self.movie_keywords, movie=self.movie, year=self.movie_year)
self.assertTrue(len(results) > 0)
def test_query_wrong_parameters(self):
with SubsWiki(self.config) as service:
self.assertRaises(ServiceError, service.query,
self.fake_file, self.languages, keywords=self.movie_keywords, movie=self.movie, series=self.series)
class SubtitulosTestCase(ServiceTestCase):
query_tests = ['test_query_series', 'test_query_wrong_series', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = []
service = Subtitulos
def setUp(self):
super(SubtitulosTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.fake_file = u'/tmp/fake_file'
self.languages = language_set(['en', 'es'])
self.episode_path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.episode_sublanguage = 'en'
self.episode_subfilesizes = [32986]
self.episode_keywords = set(['asap', 'hdtv'])
self.series = 'The Big Bang Theory'
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
class TheSubDBTestCase(ServiceTestCase):
query_tests = ['test_query', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = []
service = TheSubDB
def setUp(self):
super(TheSubDBTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.episode_path = existing_video
self.episode_sublanguage = 'en'
self.episode_subfilesizes = [33536]
self.hash = u'edc1981d6459c6111fe36205b4aff6c2'
self.languages = language_set(['en', 'nl'])
self.fake_file = u'/tmp/fake_file'
def test_query(self):
with TheSubDB(self.config) as service:
results = service.query(self.fake_file, self.hash, self.languages)
self.assertTrue(len(results) > 0)
def test_query_wrong_languages(self):
with TheSubDB(self.config) as service:
results = service.query(self.fake_file, self.hash, self.wrong_languages)
self.assertTrue(len(results) == 0)
@unittest.skipUnless(os.path.exists(existing_video), 'No existing video')
def test_list_episode(self):
super(TheSubDBTestCase, self).test_list_episode()
@unittest.skipUnless(os.path.exists(existing_video), 'No existing video')
def test_download_episode(self):
super(TheSubDBTestCase, self).test_download_episode()
class TvSubtitlesTestCase(ServiceTestCase):
query_tests = ['test_query_series', 'test_query_wrong_series', 'test_query_wrong_languages']
list_tests = ['test_list_episode', 'test_list_wrong_languages']
download_tests = ['test_download_episode']
cache_tests = ['test_cached_series']
service = TvSubtitles
def setUp(self):
super(TvSubtitlesTestCase, self).setUp()
self.config = ServiceConfig(multi=True, cache_dir=cache_dir)
self.fake_file = u'/tmp/fake_file'
self.languages = language_set(['en', 'es'])
self.episode_path = u'The Big Bang Theory/Season 05/The.Big.Bang.Theory.S05E06.HDTV.XviD-ASAP.mkv'
self.episode_sublanguage = 'en'
self.episode_subfilesizes = [33078]
self.episode_keywords = set(['asap', 'hdtv'])
self.series = 'The Big Bang Theory'
self.wrong_series = 'No Existent Show Name'
self.season = 5
self.episode = 6
def test_query_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) > 0)
def test_query_wrong_series(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.languages, self.episode_keywords, self.wrong_series, self.season, self.episode)
self.assertTrue(len(results) == 0)
def test_query_wrong_languages(self):
with self.service(self.config) as service:
results = service.query(self.fake_file, self.wrong_languages, self.episode_keywords, self.series, self.season, self.episode)
self.assertTrue(len(results) == 0)
TESTCASES = [Addic7edTestCase, BierDopjeTestCase, OpenSubtitlesTestCase, PodnapisiTestCase, SubsWikiTestCase,
SubtitulosTestCase, TheSubDBTestCase, TvSubtitlesTestCase]
def query_suite():
suite = unittest.TestSuite()
for testcase in TESTCASES:
suite.addTests(map(testcase, testcase.query_tests))
return suite
def list_suite():
suite = unittest.TestSuite()
for testcase in TESTCASES:
suite.addTests(map(testcase, testcase.list_tests))
return suite
def download_suite():
suite = unittest.TestSuite()
for testcase in TESTCASES:
suite.addTests(map(testcase, testcase.download_tests))
return suite
def cache_suite():
suite = unittest.TestSuite()
for testcase in TESTCASES:
suite.addTests(map(testcase, testcase.cache_tests))
return suite
def suite():
return unittest.TestSuite([query_suite(), list_suite(), download_suite(), cache_suite()])
if __name__ == '__main__':
unittest.TextTestRunner().run(suite())
-133
View File
@@ -1,133 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of subliminal.
#
# subliminal is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
from subliminal import Pool, list_subtitles, download_subtitles
import os
import time
import unittest
import requests
import tarfile
import StringIO
test_dir = 'test_subliminal_files'
cache_dir = 'test_subliminal_cache'
test_video = 'The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4'
def setUpModule():
if not os.path.exists(test_dir):
r = requests.get('https://github.com/downloads/Diaoul/subliminal/test_subliminal_files.tar.gz')
with tarfile.open(fileobj=StringIO.StringIO(r.content), mode='r:gz') as f:
f.extractall(test_dir)
if not os.path.exists(cache_dir):
os.mkdir(cache_dir)
class ApiTestCase(unittest.TestCase):
def test_list_subtitles(self):
results = list_subtitles(test_video, languages=['en', 'fr'], cache_dir=cache_dir)
self.assertTrue(len(results) > 0)
def test_download_subtitles(self):
results = download_subtitles(test_video, languages=['en', 'fr'], cache_dir=cache_dir)
self.assertTrue(len(results) == 1)
for video, subtitles in results.iteritems():
self.assertTrue(video.release == test_video)
self.assertTrue(len(subtitles) == 1)
for subtitle in subtitles:
self.assertTrue(os.path.exists(subtitle.path))
os.remove(subtitle.path)
def test_download_subtitles_noforce(self):
results_first = download_subtitles(test_dir, languages=['en', 'fr'], cache_dir=cache_dir, force=False, services=['thesubdb'])
results = download_subtitles(test_dir, languages=['en', 'fr'], cache_dir=cache_dir, force=False, services=['thesubdb'])
self.assertTrue(len(results) == 0)
for _, subtitles in results_first.iteritems():
for subtitle in subtitles:
os.remove(subtitle.path)
def test_download_subtitles_multi(self):
results = download_subtitles(test_video, languages=['en', 'fr'], cache_dir=cache_dir, multi=True)
self.assertTrue(len(results) == 1)
for video, subtitles in results.iteritems():
self.assertTrue(video.release == test_video)
self.assertTrue(len(subtitles) == 2)
for subtitle in subtitles:
self.assertTrue(os.path.exists(subtitle.path))
os.remove(subtitle.path)
def test_download_subtitles_multi_noforce(self):
results_first = download_subtitles(test_dir, languages=['en', 'fr'], cache_dir=cache_dir, multi=True, force=False, services=['thesubdb'])
results = download_subtitles(test_dir, languages=['en', 'fr'], cache_dir=cache_dir, multi=True, force=False, services=['thesubdb'])
self.assertTrue(len(results) == 0)
for _, subtitles in results_first.iteritems():
for subtitle in subtitles:
os.remove(subtitle.path)
def test_download_subtitles_languages(self):
results = download_subtitles('Dexter/Season 04/S04E08 - Road Kill - 720p BluRay.mkv', languages=['en'],
cache_dir=cache_dir, multi=True, force=False, services=['subtitulos', 'tvsubtitles'])
self.assertTrue(len(results) == 1)
for _, subtitles in results.iteritems():
self.assertTrue(len(subtitles) == 1)
for subtitle in subtitles:
os.remove(subtitle.path)
class AsyncTestCase(unittest.TestCase):
def test_pool(self):
p = Pool(4)
self.assertTrue(len(p.workers) == 4)
for w in p.workers:
self.assertTrue(w.isAlive() == False)
p.start()
for w in p.workers:
self.assertTrue(w.isAlive() == True)
p.stop()
p.join()
time.sleep(0.2) # so terminate is finished on Worker and proper Thread methods finished
for w in p.workers:
self.assertTrue(w.isAlive() == False)
def test_list_subtitles(self):
with Pool(4) as p:
results = p.list_subtitles(test_video, languages=['en', 'fr'], cache_dir=cache_dir)
self.assertTrue(len(results) > 0)
def test_download_subtitles(self):
with Pool(4) as p:
results = p.download_subtitles(test_video, languages=['en', 'fr'], cache_dir=cache_dir)
self.assertTrue(len(results) == 1)
for video, subtitles in results.iteritems():
self.assertTrue(video.release == test_video)
self.assertTrue(len(subtitles) == 1)
for subtitle in subtitles:
self.assertTrue(os.path.exists(subtitle.path))
os.remove(subtitle.path)
def suite():
suite = unittest.TestSuite()
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(ApiTestCase))
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AsyncTestCase))
return suite
if __name__ == '__main__':
unittest.TextTestRunner().run(suite())
-66
View File
@@ -1,66 +0,0 @@
#!/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.subtitles import EmbeddedSubtitle, ExternalSubtitle
from subliminal.videos import scan
from subliminal.language import Language
import StringIO
import os
import requests
import tarfile
import unittest
test_dir = 'test_videos_files'
def setUpModule():
if not os.path.exists(test_dir):
r = requests.get('https://github.com/downloads/Diaoul/subliminal/test_videos_files.tar.gz')
with tarfile.open(fileobj=StringIO.StringIO(r.content), mode='r:gz') as f:
f.extractall(test_dir)
class ScanTestCase(unittest.TestCase):
def test_basic(self):
results = scan(test_dir)
self.assertTrue(len(results) == 1)
self.assertTrue(isinstance(results[0], tuple))
self.assertTrue(len(results[0]) == 2)
def test_embedded_subtitles(self):
results = [s for s in scan(test_dir)[0][1] if isinstance(s, EmbeddedSubtitle)]
self.assertTrue(len(results) == 8)
for l in ('fre', 'eng', 'ita', 'spa', 'hun', 'ger', 'jpn', 'und'):
self.assertTrue(any([s.language == Language(l) for s in results]))
def test_external_subtitles(self):
results = [s for s in scan(test_dir)[0][1] if isinstance(s, ExternalSubtitle)]
self.assertTrue(len(results) == 3)
for l in ('fre', 'eng', 'und'):
self.assertTrue(any([s.language == Language(l) for s in results]))
def suite():
suite = unittest.TestSuite()
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(ScanTestCase))
return suite
if __name__ == '__main__':
unittest.TextTestRunner().run(suite())