Compare commits
416 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bd908bb35 | |||
| c00ed18560 | |||
| ea126cc17b | |||
| f52ef6d44a | |||
| b9d1a7a875 | |||
| 7c9ae3426d | |||
| c9635b3819 | |||
| ab2523c4af | |||
| 4268ab69ed | |||
| e695945f74 | |||
| 605a761bf9 | |||
| 16d151e683 | |||
| 83f0d12261 | |||
| 20082b8bff | |||
| bb6f67a4bb | |||
| 8d1a55696a | |||
| 12443b5d78 | |||
| 1f246d8c03 | |||
| 1c745c9c6a | |||
| abdb828626 | |||
| e2bda1cfce | |||
| 5d28f14978 | |||
| 25213e3d0d | |||
| c5325b19b5 | |||
| f635781c7a | |||
| 0322e920ed | |||
| fd02bbe4d6 | |||
| df321d8952 | |||
| 218318286b | |||
| 9620b71d08 | |||
| c5a911de4c | |||
| a7b9ec0837 | |||
| e2331bef2c | |||
| 14707b68ca | |||
| 37b05f710f | |||
| e842729f5e | |||
| e233448ab5 | |||
| 5bf82fa7ac | |||
| 431f71fb64 | |||
| b3a8081df7 | |||
| 67a48e07a2 | |||
| b48c0c946b | |||
| d5b4a4f226 | |||
| a71cefb583 | |||
| a413c77b32 | |||
| 5d6c19002d | |||
| a6b8068b7c | |||
| 70a647813b | |||
| ec5aaffe26 | |||
| 2e07fc17f2 | |||
| 66fd2d10ac | |||
| 5218c9ff45 | |||
| f53e6488db | |||
| d33bd854e0 | |||
| abccb4387e | |||
| b9c4c86e1f | |||
| 25ece03102 | |||
| 08fbe4b43b | |||
| 104004535f | |||
| 8727137847 | |||
| 7c2023b4b3 | |||
| 1d085475d9 | |||
| 3ebb63bc80 | |||
| c0ce9cf187 | |||
| d7649eac1f | |||
| f80eb29a5a | |||
| ecd7a6679b | |||
| b6ed77a56c | |||
| 213ffcb41a | |||
| 79de8e7f93 | |||
| a732b0c75b | |||
| a38527b722 | |||
| 1d9ad95aaf | |||
| 9785bc9746 | |||
| 328e342b9c | |||
| 7ed39c15dd | |||
| 7cca57332b | |||
| cef6cb8826 | |||
| 1faf26547b | |||
| 54555b2b16 | |||
| 152a5f12c3 | |||
| b530c0e875 | |||
| c03be0e2f0 | |||
| ceb641daa5 | |||
| 10c9ab7a8f | |||
| 63dd6a1f3c | |||
| 460528bf87 | |||
| ccb90734bd | |||
| 2ed731ce15 | |||
| c0547a63ca | |||
| 54b9439e54 | |||
| 91267191f1 | |||
| f8854fe89a | |||
| 2ee9e6333b | |||
| f6395e4022 | |||
| 160b5f8416 | |||
| 34ce399d7c | |||
| 68b7b57bbd | |||
| f40e79a78b | |||
| 2039cf78d6 | |||
| 5926fbe2bd | |||
| 5f4d83ddc2 | |||
| 25644ede67 | |||
| 2a68d04d60 | |||
| 4486c44020 | |||
| 8f39a2037f | |||
| e9e0c60bb1 | |||
| 768f4a43bc | |||
| c3c0c4584f | |||
| b5b6dc827a | |||
| 6a4124c8b1 | |||
| b709799039 | |||
| c3d85a0b6d | |||
| a8033f386c | |||
| 15993ce647 | |||
| 484788757b | |||
| b36971ed49 | |||
| b6ed4103ef | |||
| 58fb9728ad | |||
| f7526d338c | |||
| aa72a205f2 | |||
| 2d97781fe5 | |||
| d5d191847f | |||
| 8cd6a05301 | |||
| f5435b1c81 | |||
| 0a1a164cb1 | |||
| 200ff42385 | |||
| ca6a041ac8 | |||
| bf74bc0a0b | |||
| b58f072348 | |||
| a449ce3bb8 | |||
| d7d7a2c49d | |||
| 051545444b | |||
| dfb3943643 | |||
| a00af07e58 | |||
| c79908b320 | |||
| 864ecf4c7c | |||
| e0a996c090 | |||
| 8dd1f2344c | |||
| 4e326452d5 | |||
| b0bd236b40 | |||
| 202eb610af | |||
| 6dfb479da5 | |||
| b3f8a3ca4b | |||
| daacc015b8 | |||
| 62b5ab6b5a | |||
| 2984bedcd0 | |||
| e08a0fa847 | |||
| 3705d5510d | |||
| 727772fe31 | |||
| a7b13a15e5 | |||
| 9d7ed43add | |||
| 24f18379e4 | |||
| f339c90fc3 | |||
| 4c32df1f22 | |||
| 47a18336af | |||
| 5c23a0b375 | |||
| 5e8cbad452 | |||
| 2cd305ab3e | |||
| e038418bd3 | |||
| 6a1323b218 | |||
| ae1aaaddb8 | |||
| e97ad5cd06 | |||
| c0fc1e9a50 | |||
| f245383c28 | |||
| 89a2f7aaef | |||
| 78a881fe56 | |||
| 680bc12303 | |||
| 05efb826b8 | |||
| fdd0da3c43 | |||
| c9344822a3 | |||
| 0d15c2db01 | |||
| 272c36ac65 | |||
| 8c1ab6cce7 | |||
| b5c1b2e912 | |||
| 43ae916e96 | |||
| c431c85abe | |||
| 5a3971a304 | |||
| 5331c8d44e | |||
| d125ffaae5 | |||
| 29b5e782de | |||
| 163115da88 | |||
| 25a1ce1a5f | |||
| a5a7eb6092 | |||
| 17cce96721 | |||
| 615185e373 | |||
| ab29e34f5a | |||
| 101da7367e | |||
| 75faf5c0fd | |||
| 71f44a0d77 | |||
| 8ce2f70370 | |||
| bd61a43b2f | |||
| 7ffa1e6b23 | |||
| fe29f5e2f3 | |||
| cd10f3f07a | |||
| 1d39627311 | |||
| 006ef3e8c8 | |||
| f799137483 | |||
| 4dadafb87f | |||
| e85c21f40e | |||
| e5c50242fa | |||
| 2eda69d429 | |||
| 6e027133cc | |||
| eb49003bd6 | |||
| 707b189ab2 | |||
| dfb0b85877 | |||
| e0b7ce46cb | |||
| 5912f366f8 | |||
| 8b3786a875 | |||
| 815cee81c5 | |||
| 3b08b452a0 | |||
| b03f97c91e | |||
| d62a09beba | |||
| 90ebdebe99 | |||
| 2e37b94bd3 | |||
| f10cbc04e2 | |||
| 85de66bcf2 | |||
| a98b5a2a04 | |||
| 51cf652fe2 | |||
| ae117cb383 | |||
| 7ec08b2dcb | |||
| e721d6c295 | |||
| 9d9bf92a4f | |||
| fadf3d935f | |||
| 06aae43d7f | |||
| 9b81449dbd | |||
| c369e29975 | |||
| 1a00f6fe9a | |||
| b2c38e24a2 | |||
| 19b3fe8495 | |||
| 7752b92588 | |||
| cddeb3e552 | |||
| 84eb32a4f1 | |||
| 7779fc0a21 | |||
| 1c8f7c1954 | |||
| 9fc92ab065 | |||
| ea8b646c96 | |||
| bc218fa138 | |||
| 2ef474960b | |||
| fe6cfa663a | |||
| 769b16de83 | |||
| 4e8ea6587b | |||
| 4eb23d6512 | |||
| b756704121 | |||
| 192b7fce1d | |||
| 1c35aac8ce | |||
| 99a071f486 | |||
| 62e71e4c75 | |||
| e44944c538 | |||
| 2dd9409f9c | |||
| 5d2292092f | |||
| 9fb3576bb2 | |||
| 04e3512f71 | |||
| 327a40ea75 | |||
| dc59b44371 | |||
| 979bc39522 | |||
| db1c404a02 | |||
| e05297b09f | |||
| b60cb8a81f | |||
| 835dcd6eec | |||
| 26cb66aa45 | |||
| d4dc59aeba | |||
| 5258ef400f | |||
| 50ef6efcfa | |||
| 4b1cb36343 | |||
| 4324460005 | |||
| 620688d92a | |||
| 89d5ea929b | |||
| 168004dc89 | |||
| 1ad88032f2 | |||
| 4000f9e894 | |||
| 8b825263d4 | |||
| 7247568d96 | |||
| 74d5175152 | |||
| 130d961e72 | |||
| 02dd9a77f0 | |||
| f6f40d81a1 | |||
| 79fb0442dd | |||
| 56a8b2c7cf | |||
| 71a06d5bbc | |||
| 55ac0a941b | |||
| 0aaa4347f8 | |||
| 4c11e2d5c1 | |||
| 3bc239ffee | |||
| e797516468 | |||
| aa67ccd098 | |||
| b1c5ca5a00 | |||
| 050d29a186 | |||
| 7d8ce8bb30 | |||
| 38f5d303f9 | |||
| 38c86b4cf1 | |||
| 1da6d181d5 | |||
| e0788be5af | |||
| 52709a3e25 | |||
| 25cf0ac996 | |||
| 4c3d7d5b9d | |||
| b4cbfc0de2 | |||
| 600393c12a | |||
| faeeeca39a | |||
| 537d33b66a | |||
| 3933db4936 | |||
| 86c6d2c253 | |||
| 75ffe3767f | |||
| 015b7547db | |||
| 2a6d861968 | |||
| 0f188e30b0 | |||
| 5faf8277f1 | |||
| b86b4f1405 | |||
| 98f903174a | |||
| d8df8a4624 | |||
| 0df19e300f | |||
| b26bf1c470 | |||
| 53cd3f6118 | |||
| c23f955b6c | |||
| 5c5b2656ec | |||
| c6278909ff | |||
| 64d599d6f7 | |||
| 90d638e9c6 | |||
| 1dabbf78b2 | |||
| 58b0b0c756 | |||
| 1426d9d62d | |||
| 85db6c7884 | |||
| 43d02c349f | |||
| c7af4573ca | |||
| 73a217eb1b | |||
| b1854222a0 | |||
| 7cb72b2bb2 | |||
| 00245dd44d | |||
| c1972ab26f | |||
| b86254b0b8 | |||
| 00157ee655 | |||
| 6aaece1c44 | |||
| 6445064836 | |||
| 9d0c9d93a0 | |||
| 01a06166ff | |||
| f41fc2bceb | |||
| 5ffa344f1c | |||
| 31c2b21350 | |||
| b2c6562c64 | |||
| 02a36d9cad | |||
| 5dcb1916b2 | |||
| c5623ec868 | |||
| 1c17c1987f | |||
| b6318bfae0 | |||
| 0e8489951d | |||
| 21d3a1c1bb | |||
| 9055021282 | |||
| 10fcfd21b1 | |||
| c142233ff9 | |||
| e660e47265 | |||
| 40c4c0aeec | |||
| 97c9a7b025 | |||
| 9145f2530b | |||
| a1b073e5d6 | |||
| 7425153760 | |||
| c0836a94a9 | |||
| 9dc24951a2 | |||
| f3eaad8d1c | |||
| effde5014e | |||
| 74c4c06a5c | |||
| f10b7683c9 | |||
| 5e26185c9f | |||
| 2d350f5340 | |||
| f0519bbefb | |||
| 7acfb3f027 | |||
| 24621e15e3 | |||
| 3474b2363f | |||
| 560dea3e3e | |||
| f6e5cf91ab | |||
| 57e8770fda | |||
| 7e8f7e41b5 | |||
| 84d890d7b0 | |||
| e57c90b97e | |||
| 236c43b807 | |||
| bb32c286d9 | |||
| f1d4975079 | |||
| 464b783477 | |||
| c4756030c7 | |||
| bf538fee32 | |||
| cc32c29930 | |||
| 27b8703949 | |||
| 0d9bbff534 | |||
| cad60e73a6 | |||
| 1d14d21684 | |||
| 0733ef7d32 | |||
| 143f872166 | |||
| 95abab3c18 | |||
| 2e5fb46ebc | |||
| 0d11092178 | |||
| 80589f325a | |||
| 0b6b3d0905 | |||
| bc97f772b8 | |||
| efe944a10c | |||
| 9d9aed2d4b | |||
| 9cd8b7d593 | |||
| abfd2361d4 | |||
| b10e616ec2 | |||
| 7dc2a90edc | |||
| cb53199748 | |||
| 7b2402c436 | |||
| 0715437888 | |||
| bea95113e7 | |||
| e73e969f58 | |||
| fd30cf7388 | |||
| fa9792b280 | |||
| e93530c7c7 | |||
| 35d4c37d61 | |||
| 4d61d3fc42 | |||
| a24388137e | |||
| 60c7666610 | |||
| e11f1c4b28 | |||
| e1b32f237c | |||
| 60610e2032 | |||
| 277b046b41 | |||
| c823eda245 | |||
| 6340de0ddb |
+4
-1
@@ -1,5 +1,8 @@
|
||||
[report]
|
||||
exclude_lines =
|
||||
def __repr__
|
||||
pragma: no cover
|
||||
raise NotImplementedError
|
||||
def __repr__
|
||||
if __name__ == .__main__.:
|
||||
omit =
|
||||
subliminal/cli.py
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# SCM
|
||||
.git/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Pycharm
|
||||
.idea
|
||||
|
||||
# Subliminal
|
||||
docs/
|
||||
tests/
|
||||
+48
-27
@@ -1,43 +1,64 @@
|
||||
*.py[co]
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Packages
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.tox
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Pydev
|
||||
.project
|
||||
.pydevproject
|
||||
.settings
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# Rope
|
||||
.ropeproject
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Sphinx
|
||||
docs/_build
|
||||
# Pycharm
|
||||
.idea
|
||||
|
||||
# Subliminal unittests
|
||||
subliminal/tests/*.srt
|
||||
subliminal/tests/*_files
|
||||
subliminal/tests/*_cache
|
||||
# Subliminal
|
||||
tests/data/mkv/
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "docs/_themes"]
|
||||
path = docs/_themes
|
||||
url = git://github.com/Diaoul/diaoul-sphinx-themes.git
|
||||
+41
-16
@@ -1,24 +1,49 @@
|
||||
sudo: false
|
||||
|
||||
language: python
|
||||
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
|
||||
env:
|
||||
- PARSER=native
|
||||
- PARSER=lxml
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- unrar
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- python: "3.5"
|
||||
env:
|
||||
- PARSER=native
|
||||
- VCR_RECORD_MODE=all
|
||||
- PYTEST_ADDOPTS="-m integration"
|
||||
allow_failures:
|
||||
- python: "3.5"
|
||||
env:
|
||||
- PARSER=native
|
||||
- VCR_RECORD_MODE=all
|
||||
- PYTEST_ADDOPTS="-m integration"
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
- tests/data/mkv
|
||||
|
||||
before_cache:
|
||||
- rm -f $HOME/.cache/pip/log/debug.log
|
||||
|
||||
install:
|
||||
- pip install coveralls --use-mirrors
|
||||
- pip install -r requirements.txt --use-mirrors
|
||||
- pip install -e .[test]
|
||||
- if [ $PARSER = "lxml" ]; then pip install lxml; fi
|
||||
- pip install coveralls
|
||||
|
||||
script:
|
||||
- coverage run --source=subliminal setup.py test
|
||||
script: python setup.py test --addopts "--cov subliminal --verbose $PYTEST_ADDOPTS"
|
||||
|
||||
after_success:
|
||||
- coveralls
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
irc:
|
||||
channels:
|
||||
- "irc.freenode.org#subliminal"
|
||||
on_success: change
|
||||
on_failure: always
|
||||
use_notice: true
|
||||
skip_join: true
|
||||
after_success: coveralls
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
Contributing
|
||||
============
|
||||
|
||||
Issues
|
||||
------
|
||||
Issues are intended for bug report and feature requests. For any bug report please make sure to include the complete
|
||||
stack trace and DEBUG level logs as well as reproduce steps.
|
||||
|
||||
If you use the CLI, you can create a debug log file with `subliminal --debug [...] 2> debug.log`.
|
||||
|
||||
Pull Requests
|
||||
-------------
|
||||
You can contribute code and documentation with pull requests. Any code contribution must be unit tested and the pull
|
||||
request open against the *develop* branch.
|
||||
|
||||
Translations
|
||||
------------
|
||||
Contribution to translations can be made on [subliminal's transifex page](https://www.transifex.com/subliminal/subliminal/)
|
||||
Subliminal is configured to work with [transifex-client](http://docs.transifex.com/client/)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
FROM python:3
|
||||
|
||||
MAINTAINER Antoine Bertin <diaoulael@gmail.com>
|
||||
|
||||
RUN mkdir -p /usr/src/app /usr/src/cache
|
||||
WORKDIR /usr/src/app
|
||||
VOLUME /usr/src/cache
|
||||
|
||||
COPY . /usr/src/app
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
ENTRYPOINT ["subliminal", "--cache-dir", "/usr/src/cache"]
|
||||
CMD ["--help"]
|
||||
+155
-15
@@ -1,8 +1,125 @@
|
||||
Changelog
|
||||
=========
|
||||
---------
|
||||
|
||||
2.0.5
|
||||
^^^^^
|
||||
**release date:** 2016-09-03
|
||||
|
||||
* Fix addic7ed provider for some series name
|
||||
* Fix existing subtitles detection
|
||||
* Improve scoring
|
||||
* Add Docker container
|
||||
* Add .ogv video extension
|
||||
|
||||
|
||||
2.0.4
|
||||
^^^^^
|
||||
**release date:** 2016-09-03
|
||||
|
||||
* Fix subscenter
|
||||
|
||||
|
||||
2.0.3
|
||||
^^^^^
|
||||
**release date:** 2016-06-10
|
||||
|
||||
* Fix clearing cache in CLI
|
||||
|
||||
|
||||
2.0.2
|
||||
^^^^^
|
||||
**release date:** 2016-06-06
|
||||
|
||||
* Fix for dogpile.cache>=0.6.0
|
||||
* Fix missing sphinx_rtd_theme dependency
|
||||
|
||||
|
||||
2.0.1
|
||||
^^^^^
|
||||
**release date:** 2016-06-06
|
||||
|
||||
* Fix beautifulsoup4 minimal requirement
|
||||
|
||||
|
||||
2.0.0
|
||||
^^^^^
|
||||
**release date:** 2016-06-04
|
||||
|
||||
* Add refiners to enrich videos with information from metadata, tvdb and omdb
|
||||
* Add asynchronous provider search for faster searches
|
||||
* Add registrable managers so subliminal can run without install
|
||||
* Add archive support
|
||||
* Add the ability to customize scoring logic
|
||||
* Add an age argument to scan_videos for faster scanning
|
||||
* Add legendas.tv provider
|
||||
* Add shooter.cn provider
|
||||
* Improve matching and scoring
|
||||
* Improve documentation
|
||||
* Split nautilus integration into its own project
|
||||
|
||||
|
||||
1.1.1
|
||||
^^^^^
|
||||
**release date:** 2016-01-03
|
||||
|
||||
* Fix scanning videos on bad MKV files
|
||||
|
||||
|
||||
1.1
|
||||
^^^
|
||||
**release date:** 2015-12-29
|
||||
|
||||
* Fix library usage example in README
|
||||
* Fix for series name with special characters in addic7ed provider
|
||||
* Fix id property in thesubdb provider
|
||||
* Improve matching on titles
|
||||
* Add support for nautilus context menu with translations
|
||||
* Add support for searching subtitles in a separate directory
|
||||
* Add subscenter provider
|
||||
* Add support for python 3.5
|
||||
|
||||
|
||||
1.0.1
|
||||
^^^^^
|
||||
**release date:** 2015-07-23
|
||||
|
||||
* Fix unicode issues in CLI (python 2 only)
|
||||
* Fix score scaling in CLI (python 2 only)
|
||||
* Improve error handling in CLI
|
||||
* Color collect report in CLI
|
||||
|
||||
|
||||
1.0
|
||||
^^^
|
||||
**release date:** 2015-07-22
|
||||
|
||||
* Many changes and fixes
|
||||
* New test suite
|
||||
* New documentation
|
||||
* New CLI
|
||||
* Added support for SubsCenter
|
||||
|
||||
|
||||
0.7.5
|
||||
^^^^^
|
||||
**release date:** 2015-03-04
|
||||
|
||||
* Update requirements
|
||||
* Remove BierDopje provider
|
||||
* Add pre-guessed video optional argument in scan_video
|
||||
* Improve hearing impaired support
|
||||
* Fix TVSubtitles and Podnapisi providers
|
||||
|
||||
|
||||
0.7.4
|
||||
^^^^^
|
||||
**release date:** 2014-01-27
|
||||
|
||||
* Fix requirements for guessit and babelfish
|
||||
|
||||
|
||||
0.7.3
|
||||
-----
|
||||
^^^^^
|
||||
**release date:** 2013-11-22
|
||||
|
||||
* Fix windows compatibility
|
||||
@@ -12,7 +129,7 @@ Changelog
|
||||
|
||||
|
||||
0.7.2
|
||||
-----
|
||||
^^^^^
|
||||
**release date:** 2013-11-10
|
||||
|
||||
* Fix TVSubtitles for ambiguous series
|
||||
@@ -25,7 +142,7 @@ Changelog
|
||||
|
||||
|
||||
0.7.1
|
||||
-----
|
||||
^^^^^
|
||||
**release date:** 2013-11-06
|
||||
|
||||
* Improve CLI
|
||||
@@ -35,7 +152,7 @@ Changelog
|
||||
|
||||
|
||||
0.7.0
|
||||
-----
|
||||
^^^^^
|
||||
**release date:** 2013-10-29
|
||||
|
||||
**WARNING:** Complete rewrite of subliminal with backward incompatible changes
|
||||
@@ -52,8 +169,23 @@ Changelog
|
||||
* Drop a few providers
|
||||
* And much more...
|
||||
|
||||
|
||||
0.6.4
|
||||
^^^^^
|
||||
**release date:** 2013-05-19
|
||||
|
||||
* Fix requirements due to enzyme 0.3
|
||||
|
||||
|
||||
0.6.3
|
||||
^^^^^
|
||||
**release date:** 2013-01-17
|
||||
|
||||
* Fix requirements due to requests 1.0
|
||||
|
||||
|
||||
0.6.2
|
||||
-----
|
||||
^^^^^
|
||||
**release date:** 2012-09-15
|
||||
|
||||
* Fix BierDopje
|
||||
@@ -64,8 +196,9 @@ Changelog
|
||||
* Add possible services in help message of the CLI
|
||||
* Allow existing filenames to be passed without the ./ prefix
|
||||
|
||||
|
||||
0.6.1
|
||||
-----
|
||||
^^^^^
|
||||
**release date:** 2012-06-24
|
||||
|
||||
* Fix subtitle release name in BierDopje
|
||||
@@ -82,9 +215,11 @@ Changelog
|
||||
* Fix guessit.Language in Video.scan
|
||||
* Fix language detection of subtitles
|
||||
|
||||
|
||||
0.6.0
|
||||
-----
|
||||
^^^^^
|
||||
**release date:** 2012-06-16
|
||||
|
||||
**WARNING:** Backward incompatible changes
|
||||
|
||||
* Fix --workers option in CLI
|
||||
@@ -98,13 +233,14 @@ Changelog
|
||||
|
||||
|
||||
0.5.1
|
||||
-----
|
||||
^^^^^
|
||||
**release date:** 2012-03-25
|
||||
|
||||
* Improve error handling of enzyme parsing
|
||||
|
||||
|
||||
0.5
|
||||
---
|
||||
^^^
|
||||
**release date:** 2012-03-25
|
||||
**WARNING:** Backward incompatible changes
|
||||
|
||||
@@ -117,15 +253,17 @@ Changelog
|
||||
* Remove class Subliminal
|
||||
* Remove permissions handling
|
||||
|
||||
|
||||
0.4
|
||||
---
|
||||
^^^
|
||||
**release date:** 2011-11-11
|
||||
|
||||
* Many fixes
|
||||
* Better error handling
|
||||
|
||||
|
||||
0.3
|
||||
---
|
||||
^^^
|
||||
**release date:** 2011-08-18
|
||||
|
||||
* Fix a bug when series is not guessed by guessit
|
||||
@@ -135,16 +273,18 @@ Changelog
|
||||
* Add possibility to choose mode of created files
|
||||
* Add more checks before adjusting permissions
|
||||
|
||||
|
||||
0.2
|
||||
---
|
||||
^^^
|
||||
**release date:** 2011-07-11
|
||||
|
||||
* Fix plugin configuration
|
||||
* Fix some encoding issues
|
||||
* Remove extra logging
|
||||
|
||||
|
||||
0.1
|
||||
---
|
||||
**release date:** not released yet
|
||||
^^^
|
||||
**release date:** *private release*
|
||||
|
||||
* Initial release
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Antoine Bertin
|
||||
Copyright (c) 2016 Antoine Bertin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
+62
-32
@@ -1,57 +1,87 @@
|
||||
Subliminal
|
||||
==========
|
||||
Subtitles, faster than your thoughts.
|
||||
|
||||
Subliminal is a python library to search and download subtitles.
|
||||
It comes with an easy to use CLI (command-line interface) suitable for direct use or cron jobs.
|
||||
.. image:: https://img.shields.io/pypi/v/subliminal.svg
|
||||
:target: https://pypi.python.org/pypi/subliminal
|
||||
:alt: Latest Version
|
||||
|
||||
.. image:: https://travis-ci.org/Diaoul/subliminal.png?branch=develop
|
||||
.. image:: https://travis-ci.org/Diaoul/subliminal.svg?branch=develop
|
||||
:target: https://travis-ci.org/Diaoul/subliminal
|
||||
:alt: Travis CI build status
|
||||
|
||||
.. image:: https://coveralls.io/repos/Diaoul/subliminal/badge.png?branch=develop
|
||||
:target: https://coveralls.io/r/Diaoul/subliminal?branch=develop
|
||||
.. image:: https://readthedocs.org/projects/subliminal/badge/?version=latest
|
||||
:target: https://subliminal.readthedocs.org/
|
||||
:alt: Documentation Status
|
||||
|
||||
.. image:: https://coveralls.io/repos/Diaoul/subliminal/badge.svg?branch=develop&service=github
|
||||
:target: https://coveralls.io/github/Diaoul/subliminal?branch=develop
|
||||
:alt: Code coverage
|
||||
|
||||
.. image:: https://img.shields.io/github/license/Diaoul/subliminal.svg
|
||||
:target: https://github.com/Diaoul/subliminal/blob/master/LICENSE
|
||||
:alt: License
|
||||
|
||||
.. image:: https://img.shields.io/badge/gitter-join%20chat-1dce73.svg
|
||||
:alt: Join the chat at https://gitter.im/Diaoul/subliminal
|
||||
:target: https://gitter.im/Diaoul/subliminal
|
||||
|
||||
|
||||
Providers
|
||||
---------
|
||||
Subliminal uses multiple providers to give users a vast choice and have a better chance to find the
|
||||
best matching subtitles. Providers are extensible through a dedicated entry point.
|
||||
|
||||
* Addic7ed
|
||||
* BierDopje
|
||||
* OpenSubtitles
|
||||
* Podnapisi
|
||||
* TheSubDB
|
||||
* TvSubtitles
|
||||
:Project page: https://github.com/Diaoul/subliminal
|
||||
:Documentation: https://subliminal.readthedocs.org/
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
CLI
|
||||
^^^
|
||||
Download english subtitles::
|
||||
Download English subtitles::
|
||||
|
||||
$ subliminal -l en -- The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
|
||||
1 subtitle downloaded
|
||||
$ subliminal download -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
|
||||
Collecting videos [####################################] 100%
|
||||
1 video collected / 0 video ignored / 0 error
|
||||
Downloading subtitles [####################################] 100%
|
||||
Downloaded 1 subtitle
|
||||
|
||||
Library
|
||||
^^^^^^^
|
||||
Download best subtitles in French and English for videos less than one week old in a video folder,
|
||||
skipping videos that already have subtitles whether they are embedded or not::
|
||||
Download best subtitles in French and English for videos less than two weeks old in a video folder:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from babelfish import Language
|
||||
from datetime import timedelta
|
||||
import subliminal
|
||||
|
||||
from subliminal import download_best_subtitles, region, save_subtitles, scan_videos
|
||||
|
||||
# configure the cache
|
||||
subliminal.cache_region.configure('dogpile.cache.dbm', arguments={'filename': '/path/to/cachefile.dbm'})
|
||||
region.configure('dogpile.cache.dbm', arguments={'filename': 'cachefile.dbm'})
|
||||
|
||||
# scan for videos in the folder and their subtitles
|
||||
videos = subliminal.scan_videos(['/path/to/video/folder'], subtitles=True, embedded_subtitles=True, age=timedelta(weeks=1))
|
||||
# scan for videos newer than 2 weeks and their existing subtitles in a folder
|
||||
videos = scan_videos('/video/folder', age=timedelta(weeks=2))
|
||||
|
||||
# download
|
||||
subliminal.download_best_subtitles(videos, {Language('eng'), Language('fra')}, age=timedelta(week=1))
|
||||
# download best subtitles
|
||||
subtitles = download_best_subtitles(videos, {Language('eng'), Language('fra')})
|
||||
|
||||
# save them to disk, next to the video
|
||||
for v in videos:
|
||||
save_subtitles(v, subtitles[v])
|
||||
|
||||
License
|
||||
-------
|
||||
MIT
|
||||
Docker
|
||||
^^^^^^
|
||||
Run subliminal in a docker container::
|
||||
|
||||
$ docker run --rm --name subliminal -v subliminal_cache:/usr/src/cache -v /tvshows:/tvshows -it diaoulael/subliminal download -l en /tvshows/The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
|
||||
|
||||
Installation
|
||||
------------
|
||||
Subliminal can be installed as a regular python module by running::
|
||||
|
||||
$ [sudo] pip install subliminal
|
||||
|
||||
For a better isolation with your system you should use a dedicated virtualenv or install for your user only using
|
||||
the ``--user`` flag.
|
||||
|
||||
Nautilus/Nemo integration
|
||||
-------------------------
|
||||
See the dedicated `project page <https://github.com/Diaoul/nautilus-subliminal>`_ for more information.
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
sympy>=0.7.3
|
||||
sphinx>=1.1.3
|
||||
sphinxcontrib-programoutput>=0.8
|
||||
Sphinx-PyPI-upload>=0.2.1
|
||||
setuptools>=1.4
|
||||
-e .[dev,test]
|
||||
|
||||
+17
-2
@@ -2,7 +2,7 @@
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXOPTS = -n -W
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
@@ -19,7 +19,7 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@@ -30,6 +30,7 @@ help:
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " applehelp to make an Apple Help Book"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@@ -45,6 +46,7 @@ help:
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
@@ -89,6 +91,14 @@ qthelp:
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/subliminal.qhc"
|
||||
|
||||
applehelp:
|
||||
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||
@echo
|
||||
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
||||
@echo "N.B. You won't be able to view it unless you put it in" \
|
||||
"~/Library/Documentation/Help or install it in your application" \
|
||||
"bundle."
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@@ -166,6 +176,11 @@ doctest:
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
coverage:
|
||||
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||
@echo "Testing of coverage in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/coverage/python.txt."
|
||||
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
|
||||
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
<h3>Subliminal</h3>
|
||||
<p>
|
||||
Subliminal is a Python library to search and download subtitles.
|
||||
</p>
|
||||
-1
Submodule docs/_themes deleted from 24aa9748e4
@@ -1,8 +0,0 @@
|
||||
API
|
||||
===
|
||||
.. module:: subliminal.api
|
||||
|
||||
.. autodata:: PROVIDERS_ENTRY_POINT
|
||||
.. autofunction:: list_subtitles
|
||||
.. autofunction:: download_subtitles
|
||||
.. autofunction:: download_best_subtitles
|
||||
+14
-4
@@ -2,10 +2,20 @@ Cache
|
||||
=====
|
||||
.. module:: subliminal.cache
|
||||
|
||||
.. autodata:: CACHE_VERSION
|
||||
.. autofunction:: subliminal_key_generator
|
||||
.. autodata:: SHOW_EXPIRATION_TIME
|
||||
:annotation:
|
||||
|
||||
.. autodata:: EPISODE_EXPIRATION_TIME
|
||||
:annotation:
|
||||
|
||||
.. autodata:: REFINER_EXPIRATION_TIME
|
||||
:annotation:
|
||||
|
||||
.. data:: region
|
||||
:annotation:
|
||||
|
||||
The dogpile.cache region
|
||||
The :class:`~dogpile.cache.region.CacheRegion`
|
||||
|
||||
Refer to `dogpile.cache's documentation <http://dogpilecache.readthedocs.org>`_ to see how to configure the region
|
||||
|
||||
Refer to dogpile.cache's `region configuration documentation
|
||||
<http://dogpilecache.readthedocs.org/en/latest/usage.html#region-configuration>`_ to see how to configure the region
|
||||
|
||||
+1
-5
@@ -1,7 +1,3 @@
|
||||
CLI
|
||||
===
|
||||
.. module:: subliminal.cli
|
||||
|
||||
subliminal
|
||||
----------
|
||||
.. program-output:: subliminal --help
|
||||
.. automodule:: subliminal.cli
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
Core
|
||||
====
|
||||
.. automodule:: subliminal.core
|
||||
:exclude-members: ARCHIVE_EXTENSIONS
|
||||
|
||||
.. autodata:: ARCHIVE_EXTENSIONS
|
||||
:annotation:
|
||||
@@ -1,9 +1,3 @@
|
||||
Exceptions
|
||||
==========
|
||||
.. module:: subliminal.exceptions
|
||||
|
||||
.. autoclass:: Error
|
||||
.. autoclass:: ProviderError
|
||||
.. autoclass:: ProviderConfigurationError
|
||||
.. autoclass:: ProviderNotAvailable
|
||||
.. autoclass:: InvalidSubtitle
|
||||
.. automodule:: subliminal.exceptions
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Extensions
|
||||
==========
|
||||
.. automodule:: subliminal.extensions
|
||||
+45
-3
@@ -1,6 +1,48 @@
|
||||
Providers
|
||||
=========
|
||||
.. module:: subliminal.providers
|
||||
.. automodule:: subliminal.providers
|
||||
|
||||
.. autoclass:: Provider
|
||||
:members:
|
||||
Addic7ed
|
||||
--------
|
||||
.. automodule:: subliminal.providers.addic7ed
|
||||
:private-members:
|
||||
|
||||
LegendasTv
|
||||
----------
|
||||
.. automodule:: subliminal.providers.legendastv
|
||||
:private-members:
|
||||
|
||||
NapiProjekt
|
||||
-----------
|
||||
.. automodule:: subliminal.providers.napiprojekt
|
||||
:private-members:
|
||||
|
||||
OpenSubtitles
|
||||
-------------
|
||||
.. automodule:: subliminal.providers.opensubtitles
|
||||
:private-members:
|
||||
|
||||
Podnapisi
|
||||
---------
|
||||
.. automodule:: subliminal.providers.podnapisi
|
||||
:private-members:
|
||||
|
||||
Shooter
|
||||
-------
|
||||
.. automodule:: subliminal.providers.shooter
|
||||
:private-members:
|
||||
|
||||
SubsCenter
|
||||
----------
|
||||
.. automodule:: subliminal.providers.subscenter
|
||||
:private-members:
|
||||
|
||||
TheSubDB
|
||||
--------
|
||||
.. automodule:: subliminal.providers.thesubdb
|
||||
:private-members:
|
||||
|
||||
TVsubtitles
|
||||
-----------
|
||||
.. automodule:: subliminal.providers.tvsubtitles
|
||||
:private-members:
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
.. _refiners:
|
||||
|
||||
Refiners
|
||||
========
|
||||
.. automodule:: subliminal.refiners
|
||||
|
||||
|
||||
Metadata
|
||||
--------
|
||||
.. autofunction:: subliminal.refiners.metadata.refine
|
||||
|
||||
|
||||
TVDB
|
||||
----
|
||||
.. autofunction:: subliminal.refiners.tvdb.refine
|
||||
|
||||
|
||||
OMDb
|
||||
----
|
||||
.. autofunction:: subliminal.refiners.omdb.refine
|
||||
+1
-4
@@ -1,6 +1,3 @@
|
||||
Score
|
||||
=====
|
||||
.. module:: subliminal.score
|
||||
|
||||
.. autofunction:: get_episode_equations
|
||||
.. autofunction:: get_movie_equations
|
||||
.. automodule:: subliminal.score
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
Subtitle
|
||||
========
|
||||
.. module:: subliminal.subtitle
|
||||
.. automodule:: subliminal.subtitle
|
||||
:exclude-members: SUBTITLE_EXTENSIONS
|
||||
|
||||
.. autoclass:: Subtitle
|
||||
:members:
|
||||
.. autofunction:: get_subtitle_path
|
||||
.. autofunction:: is_valid_subtitle
|
||||
.. autofunction:: compute_guess_matches
|
||||
.. autodata:: SUBTITLE_EXTENSIONS
|
||||
:annotation:
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Utils
|
||||
=====
|
||||
.. automodule:: subliminal.utils
|
||||
+4
-14
@@ -1,17 +1,7 @@
|
||||
Video
|
||||
=====
|
||||
.. module:: subliminal.video
|
||||
.. automodule:: subliminal.video
|
||||
:exclude-members: VIDEO_EXTENSIONS
|
||||
|
||||
.. autodata:: VIDEO_EXTENSIONS
|
||||
.. autodata:: SUBTITLE_EXTENSIONS
|
||||
.. autoclass:: Video
|
||||
:members:
|
||||
.. autoclass:: Episode
|
||||
:members:
|
||||
.. autoclass:: Movie
|
||||
:members:
|
||||
.. autofunction:: hash_opensubtitles
|
||||
.. autofunction:: hash_thesubdb
|
||||
.. autofunction:: scan_subtitle_languages
|
||||
.. autofunction:: scan_video
|
||||
.. autofunction:: scan_videos
|
||||
.. autodata:: VIDEO_EXTENSIONS
|
||||
:annotation:
|
||||
|
||||
@@ -0,0 +1,658 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: http://podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1
|
||||
response:
|
||||
body: {string: "<html>\r\n<head><title>302 Found</title></head>\r\n<body bgcolor=\"\
|
||||
white\">\r\n<center><h1>302 Found</h1></center>\r\n<hr><center>nginx/1.8.0</center>\r\
|
||||
\n</body>\r\n</html>\r\n"}
|
||||
headers:
|
||||
Connection: [keep-alive]
|
||||
Content-Length: ['160']
|
||||
Content-Type: [text/html]
|
||||
Date: ['Wed, 23 Mar 2016 09:34:06 GMT']
|
||||
Location: ['http://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1']
|
||||
Server: [nginx/1.8.0]
|
||||
status: {code: 302, message: Moved Temporarily}
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: http://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1
|
||||
response:
|
||||
body: {string: "<html>\r\n<head><title>301 Moved Permanently</title></head>\r\n\
|
||||
<body bgcolor=\"white\">\r\n<center><h1>301 Moved Permanently</h1></center>\r\
|
||||
\n<hr><center>nginx/1.8.0</center>\r\n</body>\r\n</html>\r\n"}
|
||||
headers:
|
||||
Connection: [keep-alive]
|
||||
Content-Length: ['184']
|
||||
Content-Type: [text/html]
|
||||
Date: ['Wed, 23 Mar 2016 09:34:07 GMT']
|
||||
Location: ['https://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1']
|
||||
Server: [nginx/1.8.0]
|
||||
status: {code: 301, message: Moved Permanently}
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: https://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAA81VWW/bRhB+z6/YKC9OIXJJHT6C8QZKpNYCHDmFhbZJUARrcU0uSi0JHpIN5Md3
|
||||
hjecZNu+FHmRZma/2Tn2myG8ftjH7KCyXCfmcuS73ogps0sCbcLLUVncO+cjlhfSBDJOjLocPap8
|
||||
9Fo8g+fLm7fbD+9XLFN5GRc5+/SMMXi+ul69W222nfXkxfu3y8V28SWVoTaywChf8vKu0EWsXv4k
|
||||
0Gfo1YPYya7MMmWKMdslJf01cV7WPkOvBsnaWN+C0B1PAX22bUbsRAdjViU3Zo9KZmO2Tw5ardFa
|
||||
ZjH+pHEiA5VVhkbeyL2i9GIlcxRiacJShpVPK9eQQhOwONwiMDEkrVKdJ0FtTNVOSwyxC/Ixu0+y
|
||||
vcSi71NUMmybCVGJZYhqkBwNZZF/o04dPC1y2KjUflzVbfOnjtjOm17ZINhF63HX33+DorbacM2b
|
||||
2CDtC62tjWtR/xSwxdkiEgus5w0/7JiGOXbQbc0pGwjZZjuueWhFpNYLau5aLyBW2wBGHT9X1P8a
|
||||
NCQ3Qb5G9DPejY0NpB7krriSeaSGKf0pGDTrR0C/owQ0m0f4wFsRqnVVWWjniM5xAry7gw8vaZeP
|
||||
AB0If3Yx92eIRRlwXsXHYvE7cJKgmk+xjRR7o0P2BvcMQyXJHoHXR0ADKiaedwa8EqGZSJF/8H4B
|
||||
3mqAUyiiokhfcX48Ht00CYzEVaRdowre5pPzqHSKSDl3OnTuMBopGM2hAM6tN1/557xOj+6DfjeK
|
||||
mefPT4EPLN0pjZD465DFqj+vbNioaoNSgS4W6FKBbl2gW0dzr5bb35zrm2uq+3uYs4mXVkD3YXI6
|
||||
c5Zr/B7drm821P46ALRjusZ+z4EP1O6oSumqNKHMtDQ9pk619RBR2R/RA2Ft/nTqTfDn9IKeBQ3Q
|
||||
bnyBsToZrc0MC/+c7K1G8HpuhVfhGwVoVLmAeiTFhi+ANzLgZ6IxoAD1zAnPRf9GhmqARIQuNG4C
|
||||
uqHCC9EiIh1GnwN1r42mb3SNE8AHuG6AxPSCUu51GI4NpthRSEBLpo7c3gUSg9hMlL7Z/NDkrtN7
|
||||
Sm4c9u8Se2ES87hPyrzH/Cdyu38c9NL5+dd3/ydb/elsfkbc/LHYSmTv6EdKT7jpHB+hVy3863bu
|
||||
37blw93pCgAA
|
||||
headers:
|
||||
Connection: [keep-alive]
|
||||
Content-Encoding: [gzip]
|
||||
Content-Type: [text/xml;charset=utf-8]
|
||||
Date: ['Wed, 23 Mar 2016 09:34:07 GMT']
|
||||
Server: [nginx/1.8.0]
|
||||
Vary: [Accept-Encoding]
|
||||
status: {code: 200, message: OK}
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: http://podnapisi.net/subtitles/ZtAW/download?container=zip
|
||||
response:
|
||||
body: {string: "<html>\r\n<head><title>302 Found</title></head>\r\n<body bgcolor=\"\
|
||||
white\">\r\n<center><h1>302 Found</h1></center>\r\n<hr><center>nginx/1.8.0</center>\r\
|
||||
\n</body>\r\n</html>\r\n"}
|
||||
headers:
|
||||
Connection: [keep-alive]
|
||||
Content-Length: ['160']
|
||||
Content-Type: [text/html]
|
||||
Date: ['Wed, 23 Mar 2016 09:34:07 GMT']
|
||||
Location: ['http://www.podnapisi.net/subtitles/ZtAW/download?container=zip']
|
||||
Server: [nginx/1.8.0]
|
||||
status: {code: 302, message: Moved Temporarily}
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: http://www.podnapisi.net/subtitles/ZtAW/download?container=zip
|
||||
response:
|
||||
body: {string: "<html>\r\n<head><title>301 Moved Permanently</title></head>\r\n\
|
||||
<body bgcolor=\"white\">\r\n<center><h1>301 Moved Permanently</h1></center>\r\
|
||||
\n<hr><center>nginx/1.8.0</center>\r\n</body>\r\n</html>\r\n"}
|
||||
headers:
|
||||
Connection: [keep-alive]
|
||||
Content-Length: ['184']
|
||||
Content-Type: [text/html]
|
||||
Date: ['Wed, 23 Mar 2016 09:34:07 GMT']
|
||||
Location: ['https://www.podnapisi.net/subtitles/ZtAW/download?container=zip']
|
||||
Server: [nginx/1.8.0]
|
||||
status: {code: 301, message: Moved Permanently}
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: https://www.podnapisi.net/subtitles/ZtAW/download?container=zip
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
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
|
||||
headers:
|
||||
Accept-Ranges: [bytes]
|
||||
Connection: [keep-alive]
|
||||
Content-Disposition: [attachment; filename="e638ea178f406cb584f48051501e2cb4db4fce1d.zip"]
|
||||
Content-Length: ['10541']
|
||||
Content-Type: [application/octet-stream]
|
||||
Date: ['Wed, 23 Mar 2016 09:34:07 GMT']
|
||||
ETag: ['"4f497ffb-292d"']
|
||||
Last-Modified: ['Sun, 26 Feb 2012 00:42:35 GMT']
|
||||
Server: [nginx/1.8.0]
|
||||
status: {code: 200, message: OK}
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: http://podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1
|
||||
response:
|
||||
body: {string: "<html>\r\n<head><title>302 Found</title></head>\r\n<body bgcolor=\"\
|
||||
white\">\r\n<center><h1>302 Found</h1></center>\r\n<hr><center>nginx/1.8.0</center>\r\
|
||||
\n</body>\r\n</html>\r\n"}
|
||||
headers:
|
||||
Connection: [keep-alive]
|
||||
Content-Length: ['160']
|
||||
Content-Type: [text/html]
|
||||
Date: ['Wed, 23 Mar 2016 09:34:07 GMT']
|
||||
Location: ['http://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1']
|
||||
Server: [nginx/1.8.0]
|
||||
status: {code: 302, message: Moved Temporarily}
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: http://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1
|
||||
response:
|
||||
body: {string: "<html>\r\n<head><title>301 Moved Permanently</title></head>\r\n\
|
||||
<body bgcolor=\"white\">\r\n<center><h1>301 Moved Permanently</h1></center>\r\
|
||||
\n<hr><center>nginx/1.8.0</center>\r\n</body>\r\n</html>\r\n"}
|
||||
headers:
|
||||
Connection: [keep-alive]
|
||||
Content-Length: ['184']
|
||||
Content-Type: [text/html]
|
||||
Date: ['Wed, 23 Mar 2016 09:34:08 GMT']
|
||||
Location: ['https://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1']
|
||||
Server: [nginx/1.8.0]
|
||||
status: {code: 301, message: Moved Permanently}
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: https://www.podnapisi.net/subtitles/search/old?sTS=5&sL=hu&sTE=18&sK=The+Big+Bang+Theory&sXML=1
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAA81VWW/bRhB+z6/YKC9OIXJJHT6C8QZKpNYCHDmFhbZJUARrcU0uSi0JHpIN5Md3
|
||||
hjecZNu+FHmRZma/2Tn2myG8ftjH7KCyXCfmcuS73ogps0sCbcLLUVncO+cjlhfSBDJOjLocPap8
|
||||
9Fo8g+fLm7fbD+9XLFN5GRc5+/SMMXi+ul69W222nfXkxfu3y8V28SWVoTaywChf8vKu0EWsXv4k
|
||||
0Gfo1YPYya7MMmWKMdslJf01cV7WPkOvBsnaWN+C0B1PAX22bUbsRAdjViU3Zo9KZmO2Tw5ardFa
|
||||
ZjH+pHEiA5VVhkbeyL2i9GIlcxRiacJShpVPK9eQQhOwONwiMDEkrVKdJ0FtTNVOSwyxC/Ixu0+y
|
||||
vcSi71NUMmybCVGJZYhqkBwNZZF/o04dPC1y2KjUflzVbfOnjtjOm17ZINhF63HX33+DorbacM2b
|
||||
2CDtC62tjWtR/xSwxdkiEgus5w0/7JiGOXbQbc0pGwjZZjuueWhFpNYLau5aLyBW2wBGHT9X1P8a
|
||||
NCQ3Qb5G9DPejY0NpB7krriSeaSGKf0pGDTrR0C/owQ0m0f4wFsRqnVVWWjniM5xAry7gw8vaZeP
|
||||
AB0If3Yx92eIRRlwXsXHYvE7cJKgmk+xjRR7o0P2BvcMQyXJHoHXR0ADKiaedwa8EqGZSJF/8H4B
|
||||
3mqAUyiiokhfcX48Ht00CYzEVaRdowre5pPzqHSKSDl3OnTuMBopGM2hAM6tN1/557xOj+6DfjeK
|
||||
mefPT4EPLN0pjZD465DFqj+vbNioaoNSgS4W6FKBbl2gW0dzr5bb35zrm2uq+3uYs4mXVkD3YXI6
|
||||
c5Zr/B7drm821P46ALRjusZ+z4EP1O6oSumqNKHMtDQ9pk619RBR2R/RA2Ft/nTqTfDn9IKeBQ3Q
|
||||
bnyBsToZrc0MC/+c7K1G8HpuhVfhGwVoVLmAeiTFhi+ANzLgZ6IxoAD1zAnPRf9GhmqARIQuNG4C
|
||||
uqHCC9EiIh1GnwN1r42mb3SNE8AHuG6AxPSCUu51GI4NpthRSEBLpo7c3gUSg9hMlL7Z/NDkrtN7
|
||||
Sm4c9u8Se2ES87hPyrzH/Cdyu38c9NL5+dd3/ydb/elsfkbc/LHYSmTv6EdKT7jpHB+hVy3863bu
|
||||
37blw93pCgAA
|
||||
headers:
|
||||
Connection: [keep-alive]
|
||||
Content-Encoding: [gzip]
|
||||
Content-Type: [text/xml;charset=utf-8]
|
||||
Date: ['Wed, 23 Mar 2016 09:34:08 GMT']
|
||||
Server: [nginx/1.8.0]
|
||||
Vary: [Accept-Encoding]
|
||||
status: {code: 200, message: OK}
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: http://podnapisi.net/subtitles/ZtAW/download?container=zip
|
||||
response:
|
||||
body: {string: "<html>\r\n<head><title>302 Found</title></head>\r\n<body bgcolor=\"\
|
||||
white\">\r\n<center><h1>302 Found</h1></center>\r\n<hr><center>nginx/1.8.0</center>\r\
|
||||
\n</body>\r\n</html>\r\n"}
|
||||
headers:
|
||||
Connection: [keep-alive]
|
||||
Content-Length: ['160']
|
||||
Content-Type: [text/html]
|
||||
Date: ['Wed, 23 Mar 2016 09:34:08 GMT']
|
||||
Location: ['http://www.podnapisi.net/subtitles/ZtAW/download?container=zip']
|
||||
Server: [nginx/1.8.0]
|
||||
status: {code: 302, message: Moved Temporarily}
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: http://www.podnapisi.net/subtitles/ZtAW/download?container=zip
|
||||
response:
|
||||
body: {string: "<html>\r\n<head><title>301 Moved Permanently</title></head>\r\n\
|
||||
<body bgcolor=\"white\">\r\n<center><h1>301 Moved Permanently</h1></center>\r\
|
||||
\n<hr><center>nginx/1.8.0</center>\r\n</body>\r\n</html>\r\n"}
|
||||
headers:
|
||||
Connection: [keep-alive]
|
||||
Content-Length: ['184']
|
||||
Content-Type: [text/html]
|
||||
Date: ['Wed, 23 Mar 2016 09:34:08 GMT']
|
||||
Location: ['https://www.podnapisi.net/subtitles/ZtAW/download?container=zip']
|
||||
Server: [nginx/1.8.0]
|
||||
status: {code: 301, message: Moved Permanently}
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: https://www.podnapisi.net/subtitles/ZtAW/download?container=zip
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
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
|
||||
headers:
|
||||
Accept-Ranges: [bytes]
|
||||
Connection: [keep-alive]
|
||||
Content-Disposition: [attachment; filename="e638ea178f406cb584f48051501e2cb4db4fce1d.zip"]
|
||||
Content-Length: ['10541']
|
||||
Content-Type: [application/octet-stream]
|
||||
Date: ['Wed, 23 Mar 2016 09:34:08 GMT']
|
||||
ETag: ['"4f497ffb-292d"']
|
||||
Last-Modified: ['Sun, 26 Feb 2012 00:42:35 GMT']
|
||||
Server: [nginx/1.8.0]
|
||||
status: {code: 200, message: OK}
|
||||
version: 1
|
||||
+95
-44
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# subliminal documentation build configuration file, created by
|
||||
# sphinx-quickstart on Wed Oct 23 23:24:28 2013.
|
||||
# sphinx-quickstart on Sat Jul 11 00:40:28 2015.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
@@ -11,28 +13,41 @@
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
import sys
|
||||
import os
|
||||
import shlex
|
||||
import sphinx_rtd_theme
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
sys.path.append(os.path.abspath('_themes'))
|
||||
import subliminal
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.programoutput']
|
||||
# If true, Sphinx will warn about all references where the target cannot be found.
|
||||
nitpicky = True
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinxcontrib.programoutput',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
@@ -44,19 +59,23 @@ master_doc = 'index'
|
||||
# General information about the project.
|
||||
project = subliminal.__title__
|
||||
copyright = ' '.join(subliminal.__copyright__.split()[1:])
|
||||
author = subliminal.__copyright__.split(', ')[1]
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = subliminal.__version__
|
||||
version = subliminal.__version__.split('-')[0]
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = subliminal.__version__
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
@@ -68,7 +87,8 @@ release = subliminal.__version__
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
@@ -91,29 +111,29 @@ pygments_style = 'sphinx'
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
#keep_warnings = False
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'diaoul'
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
html_theme_options = {'github_user': 'Diaoul',
|
||||
'github_repo': 'subliminal',
|
||||
'github_branch': 'master',
|
||||
'fork_me': 1,
|
||||
'flattr': 0,
|
||||
'gittip': 'Diaoul',
|
||||
'pypi_downloads': 1,
|
||||
'pypi_version': 0,
|
||||
'travis': 0,
|
||||
'coveralls': 0}
|
||||
#html_theme_options = {}
|
||||
# html_theme_options = {
|
||||
# 'github_user': 'Diaoul',
|
||||
# 'github_repo': project,
|
||||
# 'travis_button': True,
|
||||
# 'gratipay_user': 'Diaoul'
|
||||
# }
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
html_theme_path = ['_themes']
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
@@ -136,6 +156,11 @@ html_theme_path = ['_themes']
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
@@ -145,12 +170,7 @@ html_static_path = ['_static']
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
html_sidebars = {
|
||||
'index': ['sidebar-intro.html', 'sidebar-star.html', 'sidebar-pypi.html', 'sidebar-donate.html',
|
||||
'sourcelink.html', 'searchbox.html'],
|
||||
'**': ['sidebar-intro.html', 'sidebar-star.html', 'sidebar-pypi.html', 'sidebar-donate.html',
|
||||
'localtoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html']
|
||||
}
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
@@ -182,11 +202,24 @@ html_sidebars = {
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Language to be used for generating the HTML full-text search index.
|
||||
# Sphinx supports the following languages:
|
||||
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
|
||||
# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr'
|
||||
#html_search_language = 'en'
|
||||
|
||||
# A dictionary with options for the search language support, empty by default.
|
||||
# Now only 'ja' uses this config value
|
||||
#html_search_options = {'type': 'default'}
|
||||
|
||||
# The name of a javascript file (relative to the configuration directory) that
|
||||
# implements a search results scorer. If empty, the default will be used.
|
||||
#html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'subliminaldoc'
|
||||
htmlhelp_basename = project + 'doc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
@@ -197,13 +230,17 @@ latex_elements = {
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'subliminal.tex', u'subliminal Documentation',
|
||||
u'Antoine Bertin', 'manual'),
|
||||
(master_doc, project + '.tex', project + ' Documentation',
|
||||
author, 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -227,28 +264,28 @@ latex_documents = [
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'subliminal', u'subliminal Documentation',
|
||||
[u'Antoine Bertin'], 1)
|
||||
(master_doc, project, project + ' Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output ------------------------------------------------
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'subliminal', u'subliminal Documentation',
|
||||
u'Antoine Bertin', 'subliminal', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(master_doc, project, project + ' Documentation',
|
||||
author, project, 'Subtitles, faster than your thoughts',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
@@ -264,6 +301,20 @@ texinfo_documents = [
|
||||
#texinfo_no_detailmenu = False
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
'python': ('http://docs.python.org/3.5', None),
|
||||
'guessit': ('http://guessit.readthedocs.org/en/latest', None),
|
||||
'babelfish': ('http://babelfish.readthedocs.org/en/latest', None),
|
||||
'dogpilecache': ('http://dogpilecache.readthedocs.org/en/latest', None),
|
||||
'dogpilecore': ('http://dogpilecore.readthedocs.org/en/latest', None),
|
||||
'stevedore': ('http://docs.openstack.org/developer/stevedore', None),
|
||||
'click': ('http://click.pocoo.org/4', None),
|
||||
'rarfile': ('http://rarfile.readthedocs.org/en/latest/', None)
|
||||
}
|
||||
|
||||
|
||||
# -- Options for autodoc -------------------------------------------------------
|
||||
autodoc_member_order = 'bysource'
|
||||
|
||||
autodoc_default_flags = ['members']
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
try:
|
||||
from unittest.mock import Mock
|
||||
except ImportError:
|
||||
from mock import Mock
|
||||
from vcr import VCR
|
||||
|
||||
from subliminal.cache import region
|
||||
|
||||
vcr = VCR(path_transformer=lambda path: path + '.yaml',
|
||||
record_mode=os.environ.get('VCR_RECORD_MODE', 'once'),
|
||||
match_on=['method', 'scheme', 'host', 'port', 'path', 'query', 'body'],
|
||||
cassette_library_dir=os.path.realpath(os.path.join('docs', 'cassettes')))
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope='session')
|
||||
def configure_region():
|
||||
region.configure('dogpile.cache.null')
|
||||
region.configure = Mock()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def chdir(tmpdir, monkeypatch):
|
||||
monkeypatch.chdir(str(tmpdir))
|
||||
|
||||
|
||||
@pytest.yield_fixture(autouse=True)
|
||||
def use_cassette(request):
|
||||
with vcr.use_cassette('test_' + request.fspath.purebasename):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def skip_python_2():
|
||||
if sys.version_info < (3, 0):
|
||||
return pytest.skip('Requires python 3')
|
||||
+29
-80
@@ -1,77 +1,12 @@
|
||||
.. subliminal documentation master file, created by
|
||||
sphinx-quickstart on Wed Oct 23 23:24:28 2013.
|
||||
sphinx-quickstart on Sat Jul 11 00:40:28 2015.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Subliminal
|
||||
==========
|
||||
Release v\ |version|
|
||||
|
||||
Subliminal is a python library to search and download subtitles.
|
||||
It comes with an easy to use :abbr:`CLI (command-line interface)` suitable for direct use or cron jobs.
|
||||
|
||||
|
||||
Providers
|
||||
---------
|
||||
Subliminal uses multiple providers to give users a vast choice and have a better chance to find the
|
||||
best matching subtitles. Providers are extensible through a dedicated entry point.
|
||||
|
||||
* Addic7ed
|
||||
* BierDopje
|
||||
* OpenSubtitles
|
||||
* Podnapisi
|
||||
* TheSubDB
|
||||
* TvSubtitles
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
CLI
|
||||
^^^
|
||||
Download english subtitles::
|
||||
|
||||
$ subliminal -l en -- The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
|
||||
1 subtitle downloaded
|
||||
|
||||
See :mod:`subliminal.cli`
|
||||
|
||||
Library
|
||||
^^^^^^^
|
||||
Download best subtitles in French and English for videos less than one week old in a video folder,
|
||||
skipping videos that already have subtitles whether they are embedded or not::
|
||||
|
||||
from babelfish import Language
|
||||
from datetime import timedelta
|
||||
import subliminal
|
||||
|
||||
# configure the cache
|
||||
subliminal.cache_region.configure('dogpile.cache.dbm', arguments={'filename': '/path/to/cachefile.dbm'})
|
||||
|
||||
# scan for videos in the folder and their subtitles
|
||||
videos = subliminal.scan_videos(['/path/to/video/folder'], subtitles=True, embedded_subtitles=True, age=timedelta(weeks=1))
|
||||
|
||||
# download
|
||||
subliminal.download_best_subtitles(videos, {Language('eng'), Language('fra')}, age=timedelta(week=1))
|
||||
|
||||
See :mod:`subliminal.api`, :func:`~subliminal.video.scan_videos` and :func:`~subliminal.video.scan_video`
|
||||
|
||||
How it works
|
||||
------------
|
||||
Subliminal makes use of various libraries to achieve its goal:
|
||||
|
||||
* `enzyme <http://enzyme.readthedocs.org>`_ to detect embedded subtitles in videos and retrieve metadata
|
||||
* `guessit <http://guessit.readthedocs.org>`_ to guess informations from filenames
|
||||
* `babelfish <http://babelfish.readthedocs.org>`_ to work with languages
|
||||
* `requests <http://docs.python-requests.org>`_ to make human readable HTTP requests
|
||||
* `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup>`_ to parse HTML and XML
|
||||
* `dogpile.cache <http://dogpilecache.readthedocs.org>`_ to cache intermediate search data
|
||||
* `charade <https://github.com/sigmavirus24/charade>`_ to detect subtitles' encoding
|
||||
* `pysrt <https://github.com/byroot/pysrt>`_ to validate downloaded subtitles
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
MIT
|
||||
Welcome to subliminal!
|
||||
======================
|
||||
Subliminal is a python 2.7+ library to search and download subtitles.
|
||||
It comes with an easy to use yet powerful :abbr:`CLI (command-line interface)` suitable for direct use or cron jobs.
|
||||
|
||||
|
||||
Documentation
|
||||
@@ -79,25 +14,39 @@ Documentation
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
provider_guide
|
||||
user/usage
|
||||
user/how_it_works
|
||||
user/cli
|
||||
user/provider_guide
|
||||
|
||||
|
||||
API Documentation
|
||||
-----------------
|
||||
If you are looking for information on a specific function, class or method,
|
||||
this part of the documentation is for you.
|
||||
If you are looking for information on a specific function, class or method, this part of the documentation is for you.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:maxdepth: 1
|
||||
|
||||
api/api
|
||||
api/core
|
||||
api/video
|
||||
api/subtitle
|
||||
api/providers
|
||||
api/refiners
|
||||
api/extensions
|
||||
api/score
|
||||
api/utils
|
||||
api/cache
|
||||
api/cli
|
||||
api/exceptions
|
||||
api/providers
|
||||
api/score
|
||||
api/subtitle
|
||||
api/video
|
||||
|
||||
|
||||
.. include:: ../HISTORY.rst
|
||||
License
|
||||
-------
|
||||
MIT
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
@ECHO OFF
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set BUILDDIR=_build
|
||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||
if NOT "%PAPER%" == "" (
|
||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
if "%1" == "help" (
|
||||
:help
|
||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||
echo. html to make standalone HTML files
|
||||
echo. dirhtml to make HTML files named index.html in directories
|
||||
echo. singlehtml to make a single large HTML file
|
||||
echo. pickle to make pickle files
|
||||
echo. json to make JSON files
|
||||
echo. htmlhelp to make HTML files and a HTML help project
|
||||
echo. qthelp to make HTML files and a qthelp project
|
||||
echo. devhelp to make HTML files and a Devhelp project
|
||||
echo. epub to make an epub
|
||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||
echo. text to make text files
|
||||
echo. man to make manual pages
|
||||
echo. texinfo to make Texinfo files
|
||||
echo. gettext to make PO message catalogs
|
||||
echo. changes to make an overview over all changed/added/deprecated items
|
||||
echo. xml to make Docutils-native XML files
|
||||
echo. pseudoxml to make pseudoxml-XML files for display purposes
|
||||
echo. linkcheck to check all external links for integrity
|
||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||
echo. coverage to run coverage check of the documentation if enabled
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "clean" (
|
||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||
del /q /s %BUILDDIR%\*
|
||||
goto end
|
||||
)
|
||||
|
||||
|
||||
REM Check if sphinx-build is available and fallback to Python version if any
|
||||
%SPHINXBUILD% 2> nul
|
||||
if errorlevel 9009 goto sphinx_python
|
||||
goto sphinx_ok
|
||||
|
||||
:sphinx_python
|
||||
|
||||
set SPHINXBUILD=python -m sphinx.__init__
|
||||
%SPHINXBUILD% 2> nul
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:sphinx_ok
|
||||
|
||||
|
||||
if "%1" == "html" (
|
||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dirhtml" (
|
||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "singlehtml" (
|
||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pickle" (
|
||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the pickle files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "json" (
|
||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the JSON files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "htmlhelp" (
|
||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "qthelp" (
|
||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\subliminal.qhcp
|
||||
echo.To view the help file:
|
||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\subliminal.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "devhelp" (
|
||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub" (
|
||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latex" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdf" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf
|
||||
cd %~dp0
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdfja" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf-ja
|
||||
cd %~dp0
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "text" (
|
||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "man" (
|
||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "texinfo" (
|
||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "gettext" (
|
||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "changes" (
|
||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.The overview file is in %BUILDDIR%/changes.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "linkcheck" (
|
||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Link check complete; look for any errors in the above output ^
|
||||
or in %BUILDDIR%/linkcheck/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "doctest" (
|
||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of doctests in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/doctest/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "coverage" (
|
||||
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of coverage in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/coverage/python.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "xml" (
|
||||
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The XML files are in %BUILDDIR%/xml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pseudoxml" (
|
||||
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
|
||||
goto end
|
||||
)
|
||||
|
||||
:end
|
||||
@@ -0,0 +1,18 @@
|
||||
.. _cli:
|
||||
|
||||
CLI
|
||||
===
|
||||
|
||||
subliminal
|
||||
----------
|
||||
.. program-output:: subliminal --help
|
||||
|
||||
|
||||
subliminal download
|
||||
-------------------
|
||||
.. program-output:: subliminal download --help
|
||||
|
||||
|
||||
subliminal cache
|
||||
----------------
|
||||
.. program-output:: subliminal cache --help
|
||||
@@ -0,0 +1,52 @@
|
||||
How it works
|
||||
============
|
||||
|
||||
Providers
|
||||
---------
|
||||
Subliminal uses multiple providers to give users a vast choice and have a better chance to find the best matching
|
||||
subtitles. Current supported providers are:
|
||||
|
||||
* Addic7ed
|
||||
* LegendasTV
|
||||
* NapiProjekt
|
||||
* OpenSubtitles
|
||||
* Podnapisi
|
||||
* Shooter
|
||||
* SubsCenter
|
||||
* TheSubDB
|
||||
* TvSubtitles
|
||||
|
||||
Providers all inherit the same :class:`~subliminal.providers.Provider` base class and thus share the same API.
|
||||
They are registered on the ``subliminal.providers`` entry point and are exposed through the
|
||||
:data:`~subliminal.extensions.provider_manager` for easy access.
|
||||
|
||||
To work with multiple providers seamlessly, the :class:`~subliminal.core.ProviderPool` exposes the same API but
|
||||
distributes it to its providers and :class:`~subliminal.core.AsyncProviderPool` does it asynchronously.
|
||||
|
||||
.. _scoring:
|
||||
|
||||
Scoring
|
||||
-------
|
||||
Rating subtitles and comparing them is probably the most difficult part and this is where subliminal excels with its
|
||||
powerful scoring algorithm.
|
||||
|
||||
Using `guessit <http://guessit.readthedocs.org>`_ and `enzyme <http://enzyme.readthedocs.org>`_, subliminal extracts
|
||||
properties of the video and match them with the properties of the subtitles found with the providers.
|
||||
|
||||
Equations in :mod:`subliminal.score` give a score to each property (called a match). The more matches the video and
|
||||
the subtitle have, the higher the score computed with :func:`~subliminal.score.compute_score` gets.
|
||||
|
||||
|
||||
Libraries
|
||||
---------
|
||||
Various libraries are used by subliminal and are key to its success:
|
||||
|
||||
* `guessit <http://guessit.readthedocs.org>`_ to guess information from filenames
|
||||
* `enzyme <http://enzyme.readthedocs.org>`_ to detect embedded subtitles in videos and read other video metadata
|
||||
* `babelfish <http://babelfish.readthedocs.org>`_ to work with languages
|
||||
* `requests <http://docs.python-requests.org>`_ to make human readable HTTP requests
|
||||
* `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup>`_ to parse HTML and XML
|
||||
* `dogpile.cache <http://dogpilecache.readthedocs.org>`_ to cache intermediate search results
|
||||
* `stevedore <http://docs.openstack.org/developer/stevedore/>`_ to manage the provider entry point
|
||||
* `chardet <http://chardet.readthedocs.org>`_ to detect subtitles' encoding
|
||||
* `pysrt <https://github.com/byroot/pysrt>`_ to validate downloaded subtitles
|
||||
@@ -1,6 +1,7 @@
|
||||
Provider Guide
|
||||
==============
|
||||
This guide is going to explain how to add a :class:`~subliminal.providers.Provider` to subliminal
|
||||
This guide is going to explain how to add a :class:`~subliminal.providers.Provider` to subliminal. You are encouraged
|
||||
to take a look at the existing providers, it can be a nice base to start your own provider.
|
||||
|
||||
|
||||
Requirements
|
||||
@@ -30,14 +31,15 @@ API keys must not be configurable by the user and must remain linked to sublimin
|
||||
in the provider module.
|
||||
|
||||
Per-user authentication is allowed and must be configured at instantiation as keyword arguments. Configuration
|
||||
will be done by the user through the `provider_configs` argument of the :func:`~subliminal.api.list_subtitles` and
|
||||
:func:`~subliminal.api.download_best_subtitles` functions. No network operation must be done during instantiation,
|
||||
will be done by the user through the `provider_configs` argument of the :func:`~subliminal.core.list_subtitles` and
|
||||
:func:`~subliminal.core.download_best_subtitles` functions. No network operation must be done during instantiation,
|
||||
only configuration. Any error in the configuration must raise a
|
||||
:class:`~subliminal.exceptions.ProviderConfigurationError`.
|
||||
:class:`~subliminal.exceptions.ConfigurationError`.
|
||||
|
||||
|
||||
Beyond this point, if a network error occurs, a :class:`~subliminal.exceptions.ProviderNotAvailable` exception
|
||||
must be raised and an unexpected behavior must raise a :class:`~subliminal.exceptions.ProviderError` exception.
|
||||
Beyond this point, if an error occurs, a generic :class:`~subliminal.exceptions.ProviderError` exception
|
||||
must be raised. You can also use more explicit exception classes :class:`~subliminal.exceptions.AuthenticationError`
|
||||
and :class:`~subliminal.exceptions.DownloadLimitExceeded`.
|
||||
|
||||
|
||||
Initialization / Termination
|
||||
@@ -51,14 +53,16 @@ Caching policy
|
||||
--------------
|
||||
To save bandwidth and improve querying time, intermediate data should be cached when possible. Typical use case is
|
||||
when a query to retrieve show ids is required prior to the query to actually search for subtitles. In that case
|
||||
the function that gets the show id from the show name must be cached.
|
||||
the function that gets the show id from the show name must be cached.
|
||||
Expiration time should be :data:`~subliminal.cache.SHOW_EXPIRATION_TIME` for shows and
|
||||
:data:`~subliminal.cache.EPISODE_EXPIRATION_TIME` for episodes.
|
||||
|
||||
|
||||
Language
|
||||
--------
|
||||
To be able to handle various language codes, subliminal makes use of `babelfish <http://babelfish.readthedocs.org>`_
|
||||
Language and converters. You must set the attribute :attr:`~subliminal.providers.Provider.languages` with a set of
|
||||
supported :class:`babelfish.Language`.
|
||||
supported :class:`~babelfish.language.Language`.
|
||||
|
||||
If you cannot find a suitable converter for your provider, you can `make one of your own
|
||||
<http://babelfish.readthedocs.org/en/latest/#custom-converters>`_.
|
||||
@@ -67,7 +71,7 @@ If you cannot find a suitable converter for your provider, you can `make one of
|
||||
Querying
|
||||
--------
|
||||
The :meth:`~subliminal.providers.Provider.query` method parameters must include all aspects of provider's querying with
|
||||
simple types.
|
||||
primary types.
|
||||
|
||||
|
||||
Subtitle
|
||||
@@ -79,25 +83,14 @@ It must have relevant attributes that can be used to compute the matches of the
|
||||
|
||||
Score computation
|
||||
-----------------
|
||||
|
||||
To be able to compare subtitles coming from different providers between them, the
|
||||
:meth:`~subliminal.subtitle.Subtitle.compute_matches` method must be implemented.
|
||||
If `guessit <http://guessit.readthedocs.org>`_ is used to extract data from the
|
||||
:class:`~subliminal.subtitle.Subtitle` subclass, you can use :func:`~subliminal.subtitle.compute_guess_matches`
|
||||
as a helper to compute matches between the :class:`~subliminal.video.Video` and the :class:`guessit.Guess`.
|
||||
|
||||
Refer to the `scores` attribute of :class:`~subliminal.video.Episode` and :class:`~subliminal.video.Movie`
|
||||
for a list of possible matches.
|
||||
:meth:`~subliminal.subtitle.Subtitle.get_matches` method must be implemented.
|
||||
|
||||
|
||||
Unittesting
|
||||
-----------
|
||||
All possible uses of the :meth:`~subliminal.providers.Provider.query` method must be unittested including the uses
|
||||
that produce exceptions other than :class:`~subliminal.exceptions.ProviderNotAvailable`.
|
||||
The :meth:`~subliminal.subtitle.Subtitle.compute_matches` is used to validate the unittests.
|
||||
|
||||
As it is not possible to unittest all uses of the :meth:`~subliminal.providers.Provider.list_subtitles`
|
||||
and :meth:`~subliminal.providers.Provider.download_subtitle` methods, unitests are only required to cover most common
|
||||
use cases.
|
||||
|
||||
See existing unittests for more details on how to proceed.
|
||||
All possible uses of :meth:`~subliminal.providers.Provider.query`,
|
||||
:meth:`~subliminal.providers.Provider.list_subtitles` and :meth:`~subliminal.providers.Provider.download_subtitle`
|
||||
methods must have integration tests. Use `vcrpy <https://github.com/kevin1024/vcrpy>`_ for recording and playback
|
||||
of network activity.
|
||||
Other functions must be unittested. If necessary, you can use :mod:`unittest.mock` to mock some functions.
|
||||
@@ -0,0 +1,148 @@
|
||||
Usage
|
||||
=====
|
||||
CLI
|
||||
---
|
||||
Download English subtitles::
|
||||
|
||||
$ subliminal download -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4
|
||||
Collecting videos [####################################] 100%
|
||||
1 video collected / 0 video ignored
|
||||
Downloading subtitles [####################################] 100%
|
||||
Downloaded 1 subtitle
|
||||
|
||||
.. warning::
|
||||
|
||||
For cron usage, make sure to specify a maximum age (with ``--age``) so subtitles are searched for recent videos
|
||||
only. Otherwise you will get banned from the providers for abuse due to too many requests. If subliminal didn't
|
||||
find subtitles for an old video, it's unlikely it will find subtitles for that video ever anyway.
|
||||
|
||||
See :ref:`cli` for more details on the available commands and options.
|
||||
|
||||
|
||||
Nautilus/Nemo integration
|
||||
-------------------------
|
||||
See the dedicated `project page <https://github.com/Diaoul/nautilus-subliminal>`_ for more information.
|
||||
|
||||
|
||||
High level API
|
||||
--------------
|
||||
You can call subliminal in many different ways depending on how much control you want over the process. For most use
|
||||
cases, you can stick to the standard API.
|
||||
|
||||
Common
|
||||
^^^^^^
|
||||
Let's start by importing subliminal:
|
||||
|
||||
>>> import os
|
||||
>>> from babelfish import *
|
||||
>>> from subliminal import *
|
||||
|
||||
Before going further, there are a few things to know about subliminal.
|
||||
|
||||
Video
|
||||
^^^^^
|
||||
The :class:`~subliminal.video.Movie` and :class:`~subliminal.video.Episode` classes represent a video,
|
||||
existing or not. You can create a video by name (or path) with :meth:`Video.fromname <subliminal.video.Video.fromname>`,
|
||||
use :func:`~subliminal.core.scan_video` on an existing file path to get even more information about the video or
|
||||
use :func:`~subliminal.core.scan_videos` on an existing directory path to scan a whole directory for videos.
|
||||
|
||||
>>> video = Video.fromname('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4')
|
||||
>>> video
|
||||
<Episode ['The Big Bang Theory', 5x18]>
|
||||
|
||||
Here video information was guessed based on the name of the video, you can access some video attributes:
|
||||
|
||||
>>> video.video_codec
|
||||
'h264'
|
||||
>>> video.release_group
|
||||
'LOL'
|
||||
|
||||
Configuration
|
||||
^^^^^^^^^^^^^
|
||||
Before proceeding to listing and downloading subtitles, you need to configure the cache. Subliminal uses a cache to
|
||||
reduce repeated queries to providers and improve overall performance with no impact on search quality. For the sake
|
||||
of this example, we're going to use a memory backend.
|
||||
|
||||
>>> my_region = region.configure('dogpile.cache.memory')
|
||||
|
||||
.. warning::
|
||||
|
||||
Choose a cache that fits your application and prefer persistent over volatile backends. The ``file`` backend is
|
||||
usually a good choice.
|
||||
See `dogpile.cache's documentation <http://dogpilecache.readthedocs.org>`_ for more details on backends.
|
||||
|
||||
Now that we're done with the basics, let's have some *real* fun.
|
||||
|
||||
Listing
|
||||
^^^^^^^
|
||||
To list subtitles, subliminal provides a :func:`~subliminal.core.list_subtitles` function that will return all found
|
||||
subtitles:
|
||||
|
||||
>>> subtitles = list_subtitles([video], {Language('hun')}, providers=['podnapisi'])
|
||||
>>> subtitles[video]
|
||||
[<PodnapisiSubtitle 'ZtAW' [hu]>, <PodnapisiSubtitle 'ONAW' [hu]>]
|
||||
|
||||
.. note::
|
||||
|
||||
As you noticed, all parameters are iterables but only contain one item which means you can deal with a lot of
|
||||
videos, languages and providers at the same time. For the sake of this example, we filter providers to use only one,
|
||||
pass ``providers=None`` (default) to search on all providers.
|
||||
|
||||
Scoring
|
||||
^^^^^^^
|
||||
It's usual you have multiple candidates for subtitles. To help you chose which one to download, subliminal can compare
|
||||
them to the video and tell you exactly what matches with :meth:`~subliminal.subtitle.Subtitle.get_matches`:
|
||||
|
||||
>>> for s in subtitles[video]:
|
||||
... sorted(s.get_matches(video))
|
||||
['episode', 'format', 'release_group', 'season', 'series', 'video_codec', 'year']
|
||||
['episode', 'format', 'season', 'series', 'year']
|
||||
|
||||
And then compute a score with those matches with :func:`~subliminal.score.compute_score`:
|
||||
|
||||
>>> for s in subtitles[video]:
|
||||
... {s: compute_score(s, video)}
|
||||
{<PodnapisiSubtitle 'ZtAW' [hu]>: 354}
|
||||
{<PodnapisiSubtitle 'ONAW' [hu]>: 337}
|
||||
|
||||
Now you should have a better idea about which one you should choose.
|
||||
|
||||
Downloading
|
||||
^^^^^^^^^^^
|
||||
We can settle on the first subtitle and download its content using :func:`~subliminal.core.download_subtitles`:
|
||||
|
||||
>>> subtitle = subtitles[video][0]
|
||||
>>> subtitle.content is None
|
||||
True
|
||||
>>> download_subtitles([subtitle])
|
||||
>>> subtitle.content.split(b'\n')[2]
|
||||
b'Elszaladok a boltba'
|
||||
|
||||
If you want a string instead of bytes, you can access decoded content with the
|
||||
:attr:`~subliminal.subtitle.Subtitle.text` property:
|
||||
|
||||
>>> subtitle.text.split('\n')[3]
|
||||
'néhány apróságért.'
|
||||
|
||||
Downloading best subtitles
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Downloading best subtitles is what you want to do in almost all cases, as a shortcut for listing, scoring and
|
||||
downloading you can use :func:`~subliminal.core.download_best_subtitles`:
|
||||
|
||||
>>> best_subtitles = download_best_subtitles([video], {Language('hun')}, providers=['podnapisi'])
|
||||
>>> best_subtitles[video]
|
||||
[<PodnapisiSubtitle 'ZtAW' [hu]>]
|
||||
>>> best_subtitle = best_subtitles[video][0]
|
||||
>>> best_subtitle.content.split(b'\n')[2]
|
||||
b'Elszaladok a boltba'
|
||||
|
||||
We end up with the same subtitle but with one line of code. Neat.
|
||||
|
||||
Save
|
||||
^^^^
|
||||
We got ourselves a nice subtitle, now we can save it on the file system using :func:`~subliminal.core.save_subtitles`:
|
||||
|
||||
>>> save_subtitles(video, [best_subtitle])
|
||||
[<PodnapisiSubtitle 'ZtAW' [hu]>]
|
||||
>>> os.listdir()
|
||||
['The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.hu.srt']
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
[pytest]
|
||||
norecursedirs = build dist env .tox .eggs
|
||||
addopts = --pep8 --flakes --doctest-glob='*.rst'
|
||||
pep8maxlinelength = 120
|
||||
pep8ignore =
|
||||
docs/conf.py ALL
|
||||
subliminal/__init__.py E402
|
||||
flakes-ignore =
|
||||
docs/conf.py ALL
|
||||
subliminal/__init__.py UnusedImport
|
||||
doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
|
||||
+1
-9
@@ -1,9 +1 @@
|
||||
beautifulsoup4>=4.3.2
|
||||
guessit>=0.6.2
|
||||
requests>=2.0.1
|
||||
enzyme>=0.4.0
|
||||
html5lib>=0.99
|
||||
dogpile.cache>=0.5.2
|
||||
babelfish>=0.4.0
|
||||
charade>=1.0.3
|
||||
pysrt>=0.5.0
|
||||
-e .
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
[aliases]
|
||||
test=pytest
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = docs/
|
||||
build-dir = docs/_build
|
||||
|
||||
@@ -1,40 +1,101 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
def read(*parts):
|
||||
# intentionally *not* adding an encoding option to open, See:
|
||||
# https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690
|
||||
return io.open(os.path.join(here, *parts), 'r').read()
|
||||
|
||||
|
||||
def find_version(*file_paths):
|
||||
version_file = read(*file_paths)
|
||||
version_match = re.search(r'^__version__ = [\'"]([^\'"]*)[\'"]', version_file, re.M)
|
||||
if version_match:
|
||||
return version_match.group(1)
|
||||
raise RuntimeError('Unable to find version string.')
|
||||
|
||||
|
||||
# requirements
|
||||
setup_requirements = ['pytest-runner'] if {'pytest', 'test', 'ptr'}.intersection(sys.argv) else []
|
||||
|
||||
install_requirements = ['guessit>=2.0.1', 'babelfish>=0.5.2', 'enzyme>=0.4.1', 'beautifulsoup4>=4.4.0',
|
||||
'requests>=2.0', 'click>=4.0', 'dogpile.cache>=0.6.0', 'stevedore>=1.0.0',
|
||||
'chardet>=2.3.0', 'pysrt>=1.0.1', 'six>=1.9.0', 'appdirs>=1.3', 'rarfile>=2.7',
|
||||
'pytz>=2012c']
|
||||
if sys.version_info < (3, 2):
|
||||
install_requirements.append('futures>=3.0')
|
||||
|
||||
test_requirements = ['sympy', 'vcrpy>=1.6.1', 'pytest', 'pytest-pep8', 'pytest-flakes', 'pytest-cov']
|
||||
if sys.version_info < (3, 3):
|
||||
test_requirements.append('mock')
|
||||
|
||||
dev_requirements = ['tox', 'sphinx', 'sphinx_rtd_theme', 'sphinxcontrib-programoutput', 'wheel']
|
||||
|
||||
|
||||
setup(name='subliminal',
|
||||
version='0.7.3',
|
||||
license='MIT',
|
||||
description='Subtitles, faster than your thoughts',
|
||||
long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(),
|
||||
keywords='subtitle subtitles video movie episode tv show',
|
||||
url='https://github.com/Diaoul/subliminal',
|
||||
author='Antoine Bertin',
|
||||
author_email='diaoulael@gmail.com',
|
||||
packages=find_packages(),
|
||||
classifiers=['Development Status :: 4 - Beta',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
'Topic :: Multimedia :: Video'],
|
||||
entry_points={
|
||||
'console_scripts': ['subliminal = subliminal.cli:subliminal'],
|
||||
'subliminal.providers': ['addic7ed = subliminal.providers.addic7ed:Addic7edProvider',
|
||||
'bierdopje = subliminal.providers.bierdopje:BierDopjeProvider',
|
||||
'opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider',
|
||||
'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider',
|
||||
'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider',
|
||||
'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider'],
|
||||
'babelfish.language_converters': ['addic7ed = subliminal.converters.addic7ed:Addic7edConverter',
|
||||
'podnapisi = subliminal.converters.podnapisi:PodnapisiConverter',
|
||||
'tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter']
|
||||
},
|
||||
install_requires=open('requirements.txt').readlines(),
|
||||
test_suite='subliminal.tests.suite')
|
||||
version=find_version('subliminal', '__init__.py'),
|
||||
license='MIT',
|
||||
description='Subtitles, faster than your thoughts',
|
||||
long_description=read('README.rst') + '\n\n' + read('HISTORY.rst'),
|
||||
keywords='subtitle subtitles video movie episode tv show series',
|
||||
url='https://github.com/Diaoul/subliminal',
|
||||
author='Antoine Bertin',
|
||||
author_email='diaoulael@gmail.com',
|
||||
packages=find_packages(),
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
'Topic :: Multimedia :: Video'
|
||||
],
|
||||
entry_points={
|
||||
'subliminal.providers': [
|
||||
'addic7ed = subliminal.providers.addic7ed:Addic7edProvider',
|
||||
'legendastv = subliminal.providers.legendastv:LegendasTVProvider',
|
||||
'opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider',
|
||||
'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider',
|
||||
'shooter = subliminal.providers.shooter:ShooterProvider',
|
||||
'subscenter = subliminal.providers.subscenter:SubsCenterProvider',
|
||||
'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider',
|
||||
'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider'
|
||||
],
|
||||
'subliminal.refiners': [
|
||||
'metadata = subliminal.refiners.metadata:refine',
|
||||
'omdb = subliminal.refiners.omdb:refine',
|
||||
'tvdb = subliminal.refiners.tvdb:refine'
|
||||
],
|
||||
'babelfish.language_converters': [
|
||||
'addic7ed = subliminal.converters.addic7ed:Addic7edConverter',
|
||||
'shooter = subliminal.converters.shooter:ShooterConverter',
|
||||
'thesubdb = subliminal.converters.thesubdb:TheSubDBConverter',
|
||||
'tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter'
|
||||
],
|
||||
'console_scripts': [
|
||||
'subliminal = subliminal.cli:subliminal'
|
||||
]
|
||||
},
|
||||
setup_requires=setup_requirements,
|
||||
install_requires=install_requirements,
|
||||
tests_require=test_requirements,
|
||||
extras_require={
|
||||
'test': test_requirements,
|
||||
'dev': dev_requirements
|
||||
})
|
||||
|
||||
+12
-7
@@ -1,16 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
__title__ = 'subliminal'
|
||||
__version__ = '0.7.3'
|
||||
__version__ = '2.0.5'
|
||||
__short_version__ = '.'.join(__version__.split('.')[:2])
|
||||
__author__ = 'Antoine Bertin'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = 'Copyright 2013 Antoine Bertin'
|
||||
__copyright__ = 'Copyright 2016, Antoine Bertin'
|
||||
|
||||
import logging
|
||||
from .api import PROVIDERS_ENTRY_POINT, list_subtitles, download_subtitles, download_best_subtitles
|
||||
from .cache import MutexLock, region as cache_region
|
||||
from .exceptions import Error, ProviderError, ProviderConfigurationError, ProviderNotAvailable, InvalidSubtitle
|
||||
from .subtitle import Subtitle
|
||||
from .video import VIDEO_EXTENSIONS, SUBTITLE_EXTENSIONS, Video, Episode, Movie, scan_videos, scan_video
|
||||
|
||||
from .core import (AsyncProviderPool, ProviderPool, check_video, download_best_subtitles, download_subtitles,
|
||||
list_subtitles, refine, save_subtitles, scan_video, scan_videos)
|
||||
from .cache import region
|
||||
from .exceptions import Error, ProviderError
|
||||
from .extensions import provider_manager, refiner_manager
|
||||
from .providers import Provider
|
||||
from .score import compute_score, get_scores
|
||||
from .subtitle import SUBTITLE_EXTENSIONS, Subtitle
|
||||
from .video import VIDEO_EXTENSIONS, Episode, Movie, Video
|
||||
|
||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import collections
|
||||
import io
|
||||
import logging
|
||||
import operator
|
||||
import babelfish
|
||||
import pkg_resources
|
||||
from .exceptions import ProviderNotAvailable, InvalidSubtitle
|
||||
from .subtitle import get_subtitle_path
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#: Entry point for the providers
|
||||
PROVIDERS_ENTRY_POINT = 'subliminal.providers'
|
||||
|
||||
|
||||
def list_subtitles(videos, languages, providers=None, provider_configs=None):
|
||||
"""List subtitles for `videos` with the given `languages` using the specified `providers`
|
||||
|
||||
:param videos: videos to list subtitles for
|
||||
:type videos: set of :class:`~subliminal.video.Video`
|
||||
:param languages: languages of subtitles to search for
|
||||
:type languages: set of :class:`babelfish.Language`
|
||||
:param providers: providers to use for the search, if not all
|
||||
:type providers: list of string or None
|
||||
:param provider_configs: configuration for providers
|
||||
:type provider_configs: dict of provider name => provider constructor kwargs
|
||||
:return: found subtitles
|
||||
:rtype: dict of :class:`~subliminal.video.Video` => [:class:`~subliminal.subtitle.Subtitle`]
|
||||
|
||||
"""
|
||||
provider_configs = provider_configs or {}
|
||||
subtitles = collections.defaultdict(list)
|
||||
# filter videos
|
||||
videos = [v for v in videos if v.subtitle_languages & languages < languages]
|
||||
if not videos:
|
||||
logger.info('No video to download subtitles for with languages %r', languages)
|
||||
return subtitles
|
||||
subtitle_languages = set.intersection(*[v.subtitle_languages for v in videos])
|
||||
for provider_entry_point in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT):
|
||||
# filter and initialize provider
|
||||
if providers is not None and provider_entry_point.name not in providers:
|
||||
logger.debug('Skipping provider %r: not in the list', provider_entry_point.name)
|
||||
continue
|
||||
Provider = provider_entry_point.load()
|
||||
provider_languages = Provider.languages & languages - subtitle_languages
|
||||
if not provider_languages:
|
||||
logger.info('Skipping provider %r: no language to search for', provider_entry_point.name)
|
||||
continue
|
||||
provider_videos = [v for v in videos if Provider.check(v)]
|
||||
if not provider_videos:
|
||||
logger.info('Skipping provider %r: no video to search for', provider_entry_point.name)
|
||||
continue
|
||||
|
||||
# list subtitles with the provider
|
||||
try:
|
||||
with Provider(**provider_configs.get(provider_entry_point.name, {})) as provider:
|
||||
for provider_video in provider_videos:
|
||||
provider_video_languages = provider_languages - provider_video.subtitle_languages
|
||||
if not provider_video_languages:
|
||||
logger.debug('Skipping provider %r: no language to search for for video %r',
|
||||
provider_entry_point.name, provider_video)
|
||||
continue
|
||||
logger.info('Listing subtitles with provider %r for video %r with languages %r',
|
||||
provider_entry_point.name, provider_video, provider_video_languages)
|
||||
try:
|
||||
provider_subtitles = provider.list_subtitles(provider_video, provider_video_languages)
|
||||
except ProviderNotAvailable:
|
||||
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
|
||||
break
|
||||
except:
|
||||
logger.exception('Unexpected error in provider %r', provider_entry_point.name)
|
||||
continue
|
||||
logger.info('Found %d subtitles', len(provider_subtitles))
|
||||
subtitles[provider_video].extend(provider_subtitles)
|
||||
except ProviderNotAvailable:
|
||||
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
|
||||
return subtitles
|
||||
|
||||
|
||||
def download_subtitles(subtitles, provider_configs=None, single=False):
|
||||
"""Download subtitles
|
||||
|
||||
:param subtitles: subtitles to download
|
||||
:type subtitles: dict of :class:`~subliminal.video.Video` => [:class:`~subliminal.subtitle.Subtitle`]
|
||||
:param provider_configs: configuration for providers
|
||||
:type provider_configs: dict of provider name => provider constructor kwargs
|
||||
:param bool single: download with .srt extension if `True`, add language identifier otherwise
|
||||
|
||||
"""
|
||||
provider_configs = provider_configs or {}
|
||||
discarded_providers = set()
|
||||
providers_by_name = {ep.name: ep.load() for ep in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT)}
|
||||
initialized_providers = {}
|
||||
try:
|
||||
for video, video_subtitles in subtitles.items():
|
||||
languages = {subtitle.language for subtitle in video_subtitles}
|
||||
downloaded_languages = set()
|
||||
for subtitle in video_subtitles:
|
||||
# filter
|
||||
if subtitle.language in downloaded_languages:
|
||||
continue
|
||||
if subtitle.provider_name in discarded_providers:
|
||||
logger.debug('Skipping subtitle from discarded provider %r', subtitle.provider_name)
|
||||
continue
|
||||
|
||||
# initialize provider
|
||||
if subtitle.provider_name in initialized_providers:
|
||||
provider = initialized_providers[subtitle.provider_name]
|
||||
else:
|
||||
provider = providers_by_name[subtitle.provider_name](**provider_configs.get(subtitle.provider_name, {}))
|
||||
try:
|
||||
provider.initialize()
|
||||
except ProviderNotAvailable:
|
||||
logger.warning('Provider %r is not available, discarding it', subtitle.provider_name)
|
||||
discarded_providers.add(subtitle.provider_name)
|
||||
continue
|
||||
initialized_providers[subtitle.provider_name] = provider
|
||||
|
||||
# download subtitles
|
||||
subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language)
|
||||
logger.info('Downloading subtitle %r into %r', subtitle, subtitle_path)
|
||||
try:
|
||||
subtitle_text = provider.download_subtitle(subtitle)
|
||||
except ProviderNotAvailable:
|
||||
logger.warning('Provider %r is not available, discarding it', subtitle.provider_name)
|
||||
discarded_providers.add(subtitle.provider_name)
|
||||
continue
|
||||
except InvalidSubtitle:
|
||||
logger.info('Invalid subtitle, skipping it')
|
||||
continue
|
||||
except:
|
||||
logger.exception('Unexpected error in provider %r', subtitle.provider_name)
|
||||
continue
|
||||
with io.open(subtitle_path, 'w', encoding='utf-8') as f:
|
||||
f.write(subtitle_text)
|
||||
downloaded_languages.add(subtitle.language)
|
||||
if single or downloaded_languages == languages:
|
||||
break
|
||||
finally: # terminate providers
|
||||
for (provider_name, provider) in initialized_providers.items():
|
||||
try:
|
||||
provider.terminate()
|
||||
except ProviderNotAvailable:
|
||||
logger.warning('Provider %r is not available, unable to terminate', provider_name)
|
||||
except:
|
||||
logger.exception('Unexpected error in provider %r', provider_name)
|
||||
|
||||
|
||||
def download_best_subtitles(videos, languages, providers=None, provider_configs=None, single=False, min_score=0,
|
||||
hearing_impaired=False):
|
||||
"""Download the best subtitles for `videos` with the given `languages` using the specified `providers`
|
||||
|
||||
:param videos: videos to download subtitles for
|
||||
:type videos: set of :class:`~subliminal.video.Video`
|
||||
:param languages: languages of subtitles to download
|
||||
:type languages: set of :class:`babelfish.Language`
|
||||
:param providers: providers to use for the search, if not all
|
||||
:type providers: list of string or None
|
||||
:param provider_configs: configuration for providers
|
||||
:type provider_configs: dict of provider name => provider constructor kwargs
|
||||
:param bool single: download with .srt extension if `True`, add language identifier otherwise
|
||||
:param int min_score: minimum score for subtitles to download
|
||||
:param bool hearing_impaired: download hearing impaired subtitles
|
||||
|
||||
"""
|
||||
provider_configs = provider_configs or {}
|
||||
discarded_providers = set()
|
||||
downloaded_subtitles = collections.defaultdict(list)
|
||||
# filter videos
|
||||
videos = [v for v in videos if v.subtitle_languages & languages < languages
|
||||
and (not single or babelfish.Language('und') not in v.subtitle_languages)]
|
||||
if not videos:
|
||||
logger.info('No video to download subtitles for with languages %r', languages)
|
||||
return downloaded_subtitles
|
||||
# filter and initialize providers
|
||||
subtitle_languages = set.intersection(*[v.subtitle_languages for v in videos])
|
||||
initialized_providers = {}
|
||||
for provider_entry_point in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT):
|
||||
if providers is not None and provider_entry_point.name not in providers:
|
||||
logger.debug('Skipping provider %r: not in the list', provider_entry_point.name)
|
||||
continue
|
||||
Provider = provider_entry_point.load()
|
||||
if not Provider.languages & languages - subtitle_languages:
|
||||
logger.info('Skipping provider %r: no language to search for', provider_entry_point.name)
|
||||
continue
|
||||
if not [v for v in videos if Provider.check(v)]:
|
||||
logger.info('Skipping provider %r: no video to search for', provider_entry_point.name)
|
||||
continue
|
||||
provider = Provider(**provider_configs.get(provider_entry_point.name, {}))
|
||||
try:
|
||||
provider.initialize()
|
||||
except ProviderNotAvailable:
|
||||
logger.warning('Provider %r is not available, discarding it', provider_entry_point.name)
|
||||
continue
|
||||
initialized_providers[provider_entry_point.name] = provider
|
||||
try:
|
||||
for video in videos:
|
||||
# search for subtitles
|
||||
subtitles = []
|
||||
for provider_name, provider in initialized_providers.items():
|
||||
if provider.check(video):
|
||||
if provider_name in discarded_providers:
|
||||
logger.debug('Skipping discarded provider %r', provider_name)
|
||||
continue
|
||||
provider_video_languages = provider.languages & languages - video.subtitle_languages
|
||||
if not provider_video_languages:
|
||||
logger.debug('Skipping provider %r: no language to search for for video %r', provider_name,
|
||||
video)
|
||||
continue
|
||||
logger.info('Listing subtitles with provider %r for video %r with languages %r',
|
||||
provider_name, video, provider_video_languages)
|
||||
try:
|
||||
provider_subtitles = provider.list_subtitles(video, provider_video_languages)
|
||||
except ProviderNotAvailable:
|
||||
logger.warning('Provider %r is not available, discarding it', provider_name)
|
||||
discarded_providers.add(provider_name)
|
||||
continue
|
||||
except:
|
||||
logger.exception('Unexpected error in provider %r', provider_name)
|
||||
continue
|
||||
logger.info('Found %d subtitles', len(provider_subtitles))
|
||||
subtitles.extend(provider_subtitles)
|
||||
|
||||
# find the best subtitles and download them
|
||||
downloaded_languages = video.subtitle_languages.copy()
|
||||
for subtitle, score in sorted([(s, s.compute_score(video)) for s in subtitles],
|
||||
key=operator.itemgetter(1), reverse=True):
|
||||
# filter
|
||||
if subtitle.provider_name in discarded_providers:
|
||||
logger.debug('Skipping subtitle from discarded provider %r', subtitle.provider_name)
|
||||
continue
|
||||
if subtitle.hearing_impaired != hearing_impaired:
|
||||
logger.debug('Skipping subtitle: hearing impaired != %r', hearing_impaired)
|
||||
continue
|
||||
if score < min_score:
|
||||
logger.debug('Skipping subtitle: score < %d', min_score)
|
||||
continue
|
||||
if subtitle.language in downloaded_languages:
|
||||
logger.debug('Skipping subtitle: %r already downloaded', subtitle.language)
|
||||
continue
|
||||
|
||||
# download
|
||||
provider = initialized_providers[subtitle.provider_name]
|
||||
subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language)
|
||||
logger.info('Downloading subtitle %r with score %d into %r', subtitle, score, subtitle_path)
|
||||
try:
|
||||
subtitle_text = provider.download_subtitle(subtitle)
|
||||
downloaded_subtitles[video].append(subtitle)
|
||||
except ProviderNotAvailable:
|
||||
logger.warning('Provider %r is not available, discarding it', subtitle.provider_name)
|
||||
discarded_providers.add(subtitle.provider_name)
|
||||
continue
|
||||
except InvalidSubtitle:
|
||||
logger.info('Invalid subtitle, skipping it')
|
||||
continue
|
||||
except:
|
||||
logger.exception('Unexpected error in provider %r', subtitle.provider_name)
|
||||
continue
|
||||
with io.open(subtitle_path, 'w', encoding='utf-8') as f:
|
||||
f.write(subtitle_text)
|
||||
downloaded_languages.add(subtitle.language)
|
||||
if single or downloaded_languages >= languages:
|
||||
logger.debug('All languages downloaded')
|
||||
break
|
||||
finally: # terminate providers
|
||||
for (provider_name, provider) in initialized_providers.items():
|
||||
try:
|
||||
provider.terminate()
|
||||
except ProviderNotAvailable:
|
||||
logger.warning('Provider %r is not available, unable to terminate', provider_name)
|
||||
except:
|
||||
logger.exception('Unexpected error in provider %r', provider_name)
|
||||
return downloaded_subtitles
|
||||
+13
-53
@@ -1,56 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import inspect
|
||||
from dogpile.cache import make_region # @UnresolvedImport
|
||||
from dogpile.cache.backends.file import AbstractFileLock # @UnresolvedImport
|
||||
from dogpile.cache.compat import string_type # @UnresolvedImport
|
||||
from dogpile.core.readwrite_lock import ReadWriteMutex # @UnresolvedImport
|
||||
import datetime
|
||||
|
||||
from dogpile.cache import make_region
|
||||
|
||||
#: Expiration time for show caching
|
||||
SHOW_EXPIRATION_TIME = datetime.timedelta(weeks=3).total_seconds()
|
||||
|
||||
#: Expiration time for episode caching
|
||||
EPISODE_EXPIRATION_TIME = datetime.timedelta(days=3).total_seconds()
|
||||
|
||||
#: Expiration time for scraper searches
|
||||
REFINER_EXPIRATION_TIME = datetime.timedelta(weeks=1).total_seconds()
|
||||
|
||||
|
||||
#: Subliminal's cache version
|
||||
CACHE_VERSION = 1
|
||||
|
||||
|
||||
def subliminal_key_generator(namespace, fn, to_str=string_type):
|
||||
"""Add a :data:`CACHE_VERSION` to dogpile.cache's default function_key_generator"""
|
||||
if namespace is None:
|
||||
namespace = '%d:%s:%s' % (CACHE_VERSION, fn.__module__, fn.__name__)
|
||||
else:
|
||||
namespace = '%d:%s:%s|%s' % (CACHE_VERSION, fn.__module__, fn.__name__, namespace)
|
||||
|
||||
args = inspect.getargspec(fn)
|
||||
has_self = args[0] and args[0][0] in ('self', 'cls')
|
||||
|
||||
def generate_key(*args, **kw):
|
||||
if kw:
|
||||
raise ValueError('Keyword arguments not supported')
|
||||
if has_self:
|
||||
args = args[1:]
|
||||
return namespace + '|' + ' '.join(map(to_str, args))
|
||||
return generate_key
|
||||
|
||||
|
||||
class MutexLock(AbstractFileLock):
|
||||
""":class:`MutexLock` is a thread-based rw lock based on :class:`dogpile.core.ReadWriteMutex`"""
|
||||
def __init__(self, filename):
|
||||
self.mutex = ReadWriteMutex()
|
||||
|
||||
def acquire_read_lock(self, wait):
|
||||
ret = self.mutex.acquire_read_lock(wait)
|
||||
return wait or ret
|
||||
|
||||
def acquire_write_lock(self, wait):
|
||||
ret = self.mutex.acquire_write_lock(wait)
|
||||
return wait or ret
|
||||
|
||||
def release_read_lock(self):
|
||||
return self.mutex.release_read_lock()
|
||||
|
||||
def release_write_lock(self):
|
||||
return self.mutex.release_write_lock()
|
||||
|
||||
|
||||
#: The dogpile.cache region (long-lived)
|
||||
region = make_region(function_key_generator=subliminal_key_generator)
|
||||
|
||||
#: The dogpile.cache region for :meth:`~subliminal.providers.Provider.query` (short-lived)
|
||||
query_region = make_region(function_key_generator=subliminal_key_generator)
|
||||
region = make_region()
|
||||
|
||||
+430
-136
@@ -1,167 +1,461 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals, print_function
|
||||
import argparse
|
||||
import datetime
|
||||
"""
|
||||
Subliminal uses `click <http://click.pocoo.org>`_ to provide a powerful :abbr:`CLI (command-line interface)`.
|
||||
|
||||
"""
|
||||
from __future__ import division
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import babelfish
|
||||
import guessit
|
||||
import pkg_resources
|
||||
from subliminal import (__version__, PROVIDERS_ENTRY_POINT, cache_region, MutexLock, Video, Episode, Movie, scan_videos,
|
||||
download_best_subtitles)
|
||||
try:
|
||||
import colorlog
|
||||
except ImportError:
|
||||
colorlog = None
|
||||
|
||||
from appdirs import AppDirs
|
||||
from babelfish import Error as BabelfishError, Language
|
||||
import click
|
||||
from dogpile.cache.backends.file import AbstractFileLock
|
||||
from dogpile.util.readwrite_lock import ReadWriteMutex
|
||||
from six.moves import configparser
|
||||
|
||||
from subliminal import (AsyncProviderPool, Episode, Movie, Video, __version__, check_video, compute_score, get_scores,
|
||||
provider_manager, refine, refiner_manager, region, save_subtitles, scan_video, scan_videos)
|
||||
from subliminal.core import ARCHIVE_EXTENSIONS, search_external_subtitles
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DEFAULT_CACHE_FILE = os.path.join('~', '.config', 'subliminal.cache.dbm')
|
||||
class MutexLock(AbstractFileLock):
|
||||
""":class:`MutexLock` is a thread-based rw lock based on :class:`dogpile.core.ReadWriteMutex`."""
|
||||
def __init__(self, filename):
|
||||
self.mutex = ReadWriteMutex()
|
||||
|
||||
def acquire_read_lock(self, wait):
|
||||
ret = self.mutex.acquire_read_lock(wait)
|
||||
return wait or ret
|
||||
|
||||
def acquire_write_lock(self, wait):
|
||||
ret = self.mutex.acquire_write_lock(wait)
|
||||
return wait or ret
|
||||
|
||||
def release_read_lock(self):
|
||||
return self.mutex.release_read_lock()
|
||||
|
||||
def release_write_lock(self):
|
||||
return self.mutex.release_write_lock()
|
||||
|
||||
|
||||
def subliminal():
|
||||
parser = argparse.ArgumentParser(prog='subliminal', description='Subtitles, faster than your thoughts',
|
||||
epilog='Suggestions and bug reports are greatly appreciated: '
|
||||
'https://github.com/Diaoul/subliminal/issues', add_help=False)
|
||||
class Config(object):
|
||||
"""A :class:`~configparser.ConfigParser` wrapper to store configuration.
|
||||
|
||||
# required arguments
|
||||
required_arguments_group = parser.add_argument_group('required arguments')
|
||||
required_arguments_group.add_argument('paths', nargs='+', metavar='PATH', help='path to video file or folder')
|
||||
required_arguments_group.add_argument('-l', '--languages', nargs='+', required=True, metavar='LANGUAGE',
|
||||
help='wanted languages as IETF codes e.g. fr, pt-BR, sr-Cyrl ')
|
||||
Interaction with the configuration is done with the properties.
|
||||
|
||||
# configuration
|
||||
configuration_group = parser.add_argument_group('configuration')
|
||||
configuration_group.add_argument('-s', '--single', action='store_true',
|
||||
help='download without language code in subtitle\'s filename i.e. .srt only')
|
||||
configuration_group.add_argument('-c', '--cache-file', default=DEFAULT_CACHE_FILE,
|
||||
help='cache file (default: %(default)s)')
|
||||
:param str path: path to the configuration file.
|
||||
|
||||
# filtering
|
||||
filtering_group = parser.add_argument_group('filtering')
|
||||
providers = [ep.name for ep in pkg_resources.iter_entry_points(PROVIDERS_ENTRY_POINT)]
|
||||
filtering_group.add_argument('-p', '--providers', nargs='+', metavar='PROVIDER',
|
||||
help='providers to use (%s)' % ', '.join(providers))
|
||||
filtering_group.add_argument('-m', '--min-score', type=int,
|
||||
help='minimum score for subtitles (0-%d for episodes, 0-%d for movies)'
|
||||
% (Episode.scores['hash'], Movie.scores['hash']))
|
||||
filtering_group.add_argument('-a', '--age', help='download subtitles for videos newer than AGE e.g. 12h, 1w2d')
|
||||
filtering_group.add_argument('-h', '--hearing-impaired', action='store_true',
|
||||
help='download hearing impaired subtitles')
|
||||
filtering_group.add_argument('-f', '--force', action='store_true',
|
||||
help='force subtitle download for videos with existing subtitles')
|
||||
"""
|
||||
def __init__(self, path):
|
||||
#: Path to the configuration file
|
||||
self.path = path
|
||||
|
||||
# addic7ed
|
||||
addic7ed_group = parser.add_argument_group('addic7ed')
|
||||
addic7ed_group.add_argument('--addic7ed-username', metavar='USERNAME', help='username for addic7ed provider')
|
||||
addic7ed_group.add_argument('--addic7ed-password', metavar='PASSWORD', help='password for addic7ed provider')
|
||||
#: The underlying configuration object
|
||||
self.config = configparser.SafeConfigParser()
|
||||
self.config.add_section('general')
|
||||
self.config.set('general', 'languages', json.dumps(['en']))
|
||||
self.config.set('general', 'providers', json.dumps(sorted([p.name for p in provider_manager])))
|
||||
self.config.set('general', 'refiners', json.dumps(sorted([r.name for r in refiner_manager])))
|
||||
self.config.set('general', 'single', str(0))
|
||||
self.config.set('general', 'embedded_subtitles', str(1))
|
||||
self.config.set('general', 'age', str(int(timedelta(weeks=2).total_seconds())))
|
||||
self.config.set('general', 'hearing_impaired', str(1))
|
||||
self.config.set('general', 'min_score', str(0))
|
||||
|
||||
# output
|
||||
output_group = parser.add_argument_group('output')
|
||||
output_exclusive_group = output_group.add_mutually_exclusive_group()
|
||||
output_exclusive_group.add_argument('-q', '--quiet', action='store_true', help='disable output')
|
||||
output_exclusive_group.add_argument('-v', '--verbose', action='store_true', help='verbose output')
|
||||
output_group.add_argument('--color', action='store_true', help='add color to console output (requires colorlog)')
|
||||
def read(self):
|
||||
"""Read the configuration from :attr:`path`"""
|
||||
self.config.read(self.path)
|
||||
|
||||
# troubleshooting
|
||||
troubleshooting_group = parser.add_argument_group('troubleshooting')
|
||||
troubleshooting_group.add_argument('--debug', action='store_true', help='debug output')
|
||||
troubleshooting_group.add_argument('--version', action='version', version=__version__)
|
||||
troubleshooting_group.add_argument('--help', action='help', help='show this help message and exit')
|
||||
def write(self):
|
||||
"""Write the configuration to :attr:`path`"""
|
||||
with open(self.path, 'w') as f:
|
||||
self.config.write(f)
|
||||
|
||||
# parse args
|
||||
args = parser.parse_args()
|
||||
@property
|
||||
def languages(self):
|
||||
return {Language.fromietf(l) for l in json.loads(self.config.get('general', 'languages'))}
|
||||
|
||||
# parse paths
|
||||
try:
|
||||
args.paths = [os.path.abspath(os.path.expanduser(p.decode('utf-8'))) for p in args.paths]
|
||||
except UnicodeDecodeError:
|
||||
parser.error('argument paths: encodings is not utf-8: %r' % args.paths)
|
||||
@languages.setter
|
||||
def languages(self, value):
|
||||
self.config.set('general', 'languages', json.dumps(sorted([str(l) for l in value])))
|
||||
|
||||
# parse languages
|
||||
try:
|
||||
args.languages = {babelfish.Language.fromietf(l) for l in args.languages}
|
||||
except babelfish.Error:
|
||||
parser.error('argument -l/--languages: codes are not IETF: %r' % args.languages)
|
||||
@property
|
||||
def providers(self):
|
||||
return json.loads(self.config.get('general', 'providers'))
|
||||
|
||||
# parse age
|
||||
if args.age is not None:
|
||||
match = re.match(r'^(?:(?P<weeks>\d+?)w)?(?:(?P<days>\d+?)d)?(?:(?P<hours>\d+?)h)?$', args.age)
|
||||
@providers.setter
|
||||
def providers(self, value):
|
||||
self.config.set('general', 'providers', json.dumps(sorted([p.lower() for p in value])))
|
||||
|
||||
@property
|
||||
def refiners(self):
|
||||
return json.loads(self.config.get('general', 'refiners'))
|
||||
|
||||
@refiners.setter
|
||||
def refiners(self, value):
|
||||
self.config.set('general', 'refiners', json.dumps([r.lower() for r in value]))
|
||||
|
||||
@property
|
||||
def single(self):
|
||||
return self.config.getboolean('general', 'single')
|
||||
|
||||
@single.setter
|
||||
def single(self, value):
|
||||
self.config.set('general', 'single', str(int(value)))
|
||||
|
||||
@property
|
||||
def embedded_subtitles(self):
|
||||
return self.config.getboolean('general', 'embedded_subtitles')
|
||||
|
||||
@embedded_subtitles.setter
|
||||
def embedded_subtitles(self, value):
|
||||
self.config.set('general', 'embedded_subtitles', str(int(value)))
|
||||
|
||||
@property
|
||||
def age(self):
|
||||
return timedelta(seconds=self.config.getint('general', 'age'))
|
||||
|
||||
@age.setter
|
||||
def age(self, value):
|
||||
self.config.set('general', 'age', str(int(value.total_seconds())))
|
||||
|
||||
@property
|
||||
def hearing_impaired(self):
|
||||
return self.config.getboolean('general', 'hearing_impaired')
|
||||
|
||||
@hearing_impaired.setter
|
||||
def hearing_impaired(self, value):
|
||||
self.config.set('general', 'hearing_impaired', str(int(value)))
|
||||
|
||||
@property
|
||||
def min_score(self):
|
||||
return self.config.getfloat('general', 'min_score')
|
||||
|
||||
@min_score.setter
|
||||
def min_score(self, value):
|
||||
self.config.set('general', 'min_score', str(value))
|
||||
|
||||
@property
|
||||
def provider_configs(self):
|
||||
rv = {}
|
||||
for provider in provider_manager:
|
||||
if self.config.has_section(provider.name):
|
||||
rv[provider.name] = {k: v for k, v in self.config.items(provider.name)}
|
||||
return rv
|
||||
|
||||
@provider_configs.setter
|
||||
def provider_configs(self, value):
|
||||
# loop over provider configurations
|
||||
for provider, config in value.items():
|
||||
# create the corresponding section if necessary
|
||||
if not self.config.has_section(provider):
|
||||
self.config.add_section(provider)
|
||||
|
||||
# add config options
|
||||
for k, v in config.items():
|
||||
self.config.set(provider, k, v)
|
||||
|
||||
|
||||
class LanguageParamType(click.ParamType):
|
||||
""":class:`~click.ParamType` for languages that returns a :class:`~babelfish.language.Language`"""
|
||||
name = 'language'
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
return Language.fromietf(value)
|
||||
except BabelfishError:
|
||||
self.fail('%s is not a valid language' % value)
|
||||
|
||||
LANGUAGE = LanguageParamType()
|
||||
|
||||
|
||||
class AgeParamType(click.ParamType):
|
||||
""":class:`~click.ParamType` for age strings that returns a :class:`~datetime.timedelta`
|
||||
|
||||
An age string is in the form `number + identifier` with possible identifiers:
|
||||
|
||||
* ``w`` for weeks
|
||||
* ``d`` for days
|
||||
* ``h`` for hours
|
||||
|
||||
The form can be specified multiple times but only with that idenfier ordering. For example:
|
||||
|
||||
* ``1w2d4h`` for 1 week, 2 days and 4 hours
|
||||
* ``2w`` for 2 weeks
|
||||
* ``3w6h`` for 3 weeks and 6 hours
|
||||
|
||||
"""
|
||||
name = 'age'
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
match = re.match(r'^(?:(?P<weeks>\d+?)w)?(?:(?P<days>\d+?)d)?(?:(?P<hours>\d+?)h)?$', value)
|
||||
if not match:
|
||||
parser.error('argument -a/--age: invalid age: %r' % args.age)
|
||||
args.age = datetime.timedelta(**{k: int(v) for k, v in match.groupdict(0).items()})
|
||||
self.fail('%s is not a valid age' % value)
|
||||
|
||||
# parse cache-file
|
||||
args.cache_file = os.path.abspath(os.path.expanduser(args.cache_file))
|
||||
if not os.path.exists(os.path.split(args.cache_file)[0]):
|
||||
parser.error('argument -c/--cache-file: directory %r for cache file does not exist'
|
||||
% os.path.split(args.cache_file)[0])
|
||||
return timedelta(**{k: int(v) for k, v in match.groupdict(0).items()})
|
||||
|
||||
# parse provider configs
|
||||
provider_configs = {}
|
||||
if (args.addic7ed_username is not None and args.addic7ed_password is None
|
||||
or args.addic7ed_username is None and args.addic7ed_password is not None):
|
||||
parser.error('argument --addic7ed-username/--addic7ed-password: both arguments are required or none')
|
||||
if args.addic7ed_username is not None and args.addic7ed_password is not None:
|
||||
provider_configs['addic7ed'] = {'username': args.addic7ed_username, 'password': args.addic7ed_password}
|
||||
AGE = AgeParamType()
|
||||
|
||||
# parse color
|
||||
if args.color and colorlog is None:
|
||||
parser.error('argument --color: colorlog required')
|
||||
PROVIDER = click.Choice(sorted(provider_manager.names()))
|
||||
|
||||
# setup output
|
||||
if args.debug:
|
||||
handler = logging.StreamHandler()
|
||||
if args.color:
|
||||
handler.setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)-8s%(reset)s [%(blue)s%(name)s-%(funcName)s:%(lineno)d%(reset)s] %(message)s',
|
||||
log_colors=dict(colorlog.default_log_colors.items() + [('DEBUG', 'cyan')])))
|
||||
else:
|
||||
handler.setFormatter(logging.Formatter('%(levelname)-8s [%(name)s-%(funcName)s:%(lineno)d] %(message)s'))
|
||||
logging.getLogger().addHandler(handler)
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
elif args.verbose:
|
||||
handler = logging.StreamHandler()
|
||||
if args.color:
|
||||
handler.setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)-8s%(reset)s [%(blue)s%(name)s%(reset)s] %(message)s'))
|
||||
else:
|
||||
handler.setFormatter(logging.Formatter('%(levelname)-8s [%(name)s] %(message)s'))
|
||||
logging.getLogger('subliminal').addHandler(handler)
|
||||
logging.getLogger('subliminal').setLevel(logging.INFO)
|
||||
elif not args.quiet:
|
||||
handler = logging.StreamHandler()
|
||||
if args.color:
|
||||
handler.setFormatter(colorlog.ColoredFormatter('[%(log_color)s%(levelname)s%(reset)s] %(message)s'))
|
||||
else:
|
||||
handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
|
||||
logging.getLogger('subliminal.api').addHandler(handler)
|
||||
logging.getLogger('subliminal.api').setLevel(logging.INFO)
|
||||
REFINER = click.Choice(sorted(refiner_manager.names()))
|
||||
|
||||
dirs = AppDirs('subliminal')
|
||||
cache_file = 'subliminal.dbm'
|
||||
config_file = 'config.ini'
|
||||
|
||||
|
||||
@click.group(context_settings={'max_content_width': 100}, epilog='Suggestions and bug reports are greatly appreciated: '
|
||||
'https://github.com/Diaoul/subliminal/')
|
||||
@click.option('--addic7ed', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD', help='Addic7ed configuration.')
|
||||
@click.option('--legendastv', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD', help='LegendasTV configuration.')
|
||||
@click.option('--opensubtitles', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD',
|
||||
help='OpenSubtitles configuration.')
|
||||
@click.option('--subscenter', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD', help='SubsCenter configuration.')
|
||||
@click.option('--cache-dir', type=click.Path(writable=True, file_okay=False), default=dirs.user_cache_dir,
|
||||
show_default=True, expose_value=True, help='Path to the cache directory.')
|
||||
@click.option('--debug', is_flag=True, help='Print useful information for debugging subliminal and for reporting bugs.')
|
||||
@click.version_option(__version__)
|
||||
@click.pass_context
|
||||
def subliminal(ctx, addic7ed, legendastv, opensubtitles, subscenter, cache_dir, debug):
|
||||
"""Subtitles, faster than your thoughts."""
|
||||
# create cache directory
|
||||
try:
|
||||
os.makedirs(cache_dir)
|
||||
except OSError:
|
||||
if not os.path.isdir(cache_dir):
|
||||
raise
|
||||
|
||||
# configure cache
|
||||
cache_region.configure('dogpile.cache.dbm', expiration_time=datetime.timedelta(days=30), # @UndefinedVariable
|
||||
arguments={'filename': args.cache_file, 'lock_factory': MutexLock})
|
||||
region.configure('dogpile.cache.dbm', expiration_time=timedelta(days=30),
|
||||
arguments={'filename': os.path.join(cache_dir, cache_file), 'lock_factory': MutexLock})
|
||||
|
||||
# configure logging
|
||||
if debug:
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
|
||||
logging.getLogger('subliminal').addHandler(handler)
|
||||
logging.getLogger('subliminal').setLevel(logging.DEBUG)
|
||||
|
||||
# provider configs
|
||||
ctx.obj = {'provider_configs': {}}
|
||||
if addic7ed:
|
||||
ctx.obj['provider_configs']['addic7ed'] = {'username': addic7ed[0], 'password': addic7ed[1]}
|
||||
if legendastv:
|
||||
ctx.obj['provider_configs']['legendastv'] = {'username': legendastv[0], 'password': legendastv[1]}
|
||||
if opensubtitles:
|
||||
ctx.obj['provider_configs']['opensubtitles'] = {'username': opensubtitles[0], 'password': opensubtitles[1]}
|
||||
if subscenter:
|
||||
ctx.obj['provider_configs']['subscenter'] = {'username': subscenter[0], 'password': subscenter[1]}
|
||||
|
||||
|
||||
@subliminal.command()
|
||||
@click.option('--clear-subliminal', is_flag=True, help='Clear subliminal\'s cache. Use this ONLY if your cache is '
|
||||
'corrupted or if you experience issues.')
|
||||
@click.pass_context
|
||||
def cache(ctx, clear_subliminal):
|
||||
"""Cache management."""
|
||||
if clear_subliminal:
|
||||
for file in glob.glob(os.path.join(ctx.parent.params['cache_dir'], cache_file) + '*'):
|
||||
os.remove(file)
|
||||
click.echo('Subliminal\'s cache cleared.')
|
||||
else:
|
||||
click.echo('Nothing done.')
|
||||
|
||||
|
||||
@subliminal.command()
|
||||
@click.option('-l', '--language', type=LANGUAGE, required=True, multiple=True, help='Language as IETF code, '
|
||||
'e.g. en, pt-BR (can be used multiple times).')
|
||||
@click.option('-p', '--provider', type=PROVIDER, multiple=True, help='Provider to use (can be used multiple times).')
|
||||
@click.option('-r', '--refiner', type=REFINER, multiple=True, help='Refiner to use (can be used multiple times).')
|
||||
@click.option('-a', '--age', type=AGE, help='Filter videos newer than AGE, e.g. 12h, 1w2d.')
|
||||
@click.option('-d', '--directory', type=click.STRING, metavar='DIR', help='Directory where to save subtitles, '
|
||||
'default is next to the video file.')
|
||||
@click.option('-e', '--encoding', type=click.STRING, metavar='ENC', help='Subtitle file encoding, default is to '
|
||||
'preserve original encoding.')
|
||||
@click.option('-s', '--single', is_flag=True, default=False, help='Save subtitle without language code in the file '
|
||||
'name, i.e. use .srt extension. Do not use this unless your media player requires it.')
|
||||
@click.option('-f', '--force', is_flag=True, default=False, help='Force download even if a subtitle already exist.')
|
||||
@click.option('-hi', '--hearing-impaired', is_flag=True, default=False, help='Prefer hearing impaired subtitles.')
|
||||
@click.option('-m', '--min-score', type=click.IntRange(0, 100), default=0, help='Minimum score for a subtitle '
|
||||
'to be downloaded (0 to 100).')
|
||||
@click.option('-w', '--max-workers', type=click.IntRange(1, 50), default=None, help='Maximum number of threads to use.')
|
||||
@click.option('-z/-Z', '--archives/--no-archives', default=True, show_default=True, help='Scan archives for videos '
|
||||
'(supported extensions: %s).' % ', '.join(ARCHIVE_EXTENSIONS))
|
||||
@click.option('-v', '--verbose', count=True, help='Increase verbosity.')
|
||||
@click.argument('path', type=click.Path(), required=True, nargs=-1)
|
||||
@click.pass_obj
|
||||
def download(obj, provider, refiner, language, age, directory, encoding, single, force, hearing_impaired, min_score,
|
||||
max_workers, archives, verbose, path):
|
||||
"""Download best subtitles.
|
||||
|
||||
PATH can be an directory containing videos, a video file path or a video file name. It can be used multiple times.
|
||||
|
||||
If an existing subtitle is detected (external or embedded) in the correct language, the download is skipped for
|
||||
the associated video.
|
||||
|
||||
"""
|
||||
# process parameters
|
||||
language = set(language)
|
||||
|
||||
# scan videos
|
||||
videos = scan_videos([p for p in args.paths if os.path.exists(p)], subtitles=not args.force,
|
||||
embedded_subtitles=not args.force, age=args.age)
|
||||
videos = []
|
||||
ignored_videos = []
|
||||
errored_paths = []
|
||||
with click.progressbar(path, label='Collecting videos', item_show_func=lambda p: p or '') as bar:
|
||||
for p in bar:
|
||||
logger.debug('Collecting path %s', p)
|
||||
|
||||
# guess videos
|
||||
videos.extend([Video.fromguess(os.path.split(p)[1], guessit.guess_file_info(p, 'autodetect')) for p in args.paths
|
||||
if not os.path.exists(p)])
|
||||
# non-existing
|
||||
if not os.path.exists(p):
|
||||
try:
|
||||
video = Video.fromname(p)
|
||||
except:
|
||||
logger.exception('Unexpected error while collecting non-existing path %s', p)
|
||||
errored_paths.append(p)
|
||||
continue
|
||||
if not force:
|
||||
video.subtitle_languages |= set(search_external_subtitles(video.name, directory=directory).values())
|
||||
refine(video, episode_refiners=refiner, movie_refiners=refiner, embedded_subtitles=not force)
|
||||
videos.append(video)
|
||||
continue
|
||||
|
||||
# directories
|
||||
if os.path.isdir(p):
|
||||
try:
|
||||
scanned_videos = scan_videos(p, age=age, archives=archives)
|
||||
except:
|
||||
logger.exception('Unexpected error while collecting directory path %s', p)
|
||||
errored_paths.append(p)
|
||||
continue
|
||||
for video in scanned_videos:
|
||||
if not force:
|
||||
video.subtitle_languages |= set(search_external_subtitles(video.name,
|
||||
directory=directory).values())
|
||||
if check_video(video, languages=language, age=age, undefined=single):
|
||||
refine(video, episode_refiners=refiner, movie_refiners=refiner, embedded_subtitles=not force)
|
||||
videos.append(video)
|
||||
else:
|
||||
ignored_videos.append(video)
|
||||
continue
|
||||
|
||||
# other inputs
|
||||
try:
|
||||
video = scan_video(p)
|
||||
except:
|
||||
logger.exception('Unexpected error while collecting path %s', p)
|
||||
errored_paths.append(p)
|
||||
continue
|
||||
if not force:
|
||||
video.subtitle_languages |= set(search_external_subtitles(video.name, directory=directory).values())
|
||||
if check_video(video, languages=language, age=age, undefined=single):
|
||||
refine(video, episode_refiners=refiner, movie_refiners=refiner, embedded_subtitles=not force)
|
||||
videos.append(video)
|
||||
else:
|
||||
ignored_videos.append(video)
|
||||
|
||||
# output errored paths
|
||||
if verbose > 0:
|
||||
for p in errored_paths:
|
||||
click.secho('%s errored' % p, fg='red')
|
||||
|
||||
# output ignored videos
|
||||
if verbose > 1:
|
||||
for video in ignored_videos:
|
||||
click.secho('%s ignored - subtitles: %s / age: %d day%s' % (
|
||||
os.path.split(video.name)[1],
|
||||
', '.join(str(s) for s in video.subtitle_languages) or 'none',
|
||||
video.age.days,
|
||||
's' if video.age.days > 1 else ''
|
||||
), fg='yellow')
|
||||
|
||||
# report collected videos
|
||||
click.echo('%s video%s collected / %s video%s ignored / %s error%s' % (
|
||||
click.style(str(len(videos)), bold=True, fg='green' if videos else None),
|
||||
's' if len(videos) > 1 else '',
|
||||
click.style(str(len(ignored_videos)), bold=True, fg='yellow' if ignored_videos else None),
|
||||
's' if len(ignored_videos) > 1 else '',
|
||||
click.style(str(len(errored_paths)), bold=True, fg='red' if errored_paths else None),
|
||||
's' if len(errored_paths) > 1 else '',
|
||||
))
|
||||
|
||||
# exit if no video collected
|
||||
if not videos:
|
||||
return
|
||||
|
||||
# download best subtitles
|
||||
subtitles = download_best_subtitles(videos, args.languages, providers=args.providers,
|
||||
provider_configs=provider_configs, single=args.single,
|
||||
min_score=args.min_score, hearing_impaired=args.hearing_impaired)
|
||||
downloaded_subtitles = defaultdict(list)
|
||||
with AsyncProviderPool(max_workers=max_workers, providers=provider, provider_configs=obj['provider_configs']) as p:
|
||||
with click.progressbar(videos, label='Downloading subtitles',
|
||||
item_show_func=lambda v: os.path.split(v.name)[1] if v is not None else '') as bar:
|
||||
for v in bar:
|
||||
scores = get_scores(v)
|
||||
subtitles = p.download_best_subtitles(p.list_subtitles(v, language - v.subtitle_languages),
|
||||
v, language, min_score=scores['hash'] * min_score / 100,
|
||||
hearing_impaired=hearing_impaired, only_one=single)
|
||||
downloaded_subtitles[v] = subtitles
|
||||
|
||||
# result output
|
||||
if not subtitles:
|
||||
if not args.quiet:
|
||||
sys.stderr.write('No subtitles downloaded\n')
|
||||
exit(1)
|
||||
if not args.quiet:
|
||||
subtitles_count = sum([len(s) for s in subtitles.values()])
|
||||
if subtitles_count == 1:
|
||||
print('%d subtitle downloaded' % subtitles_count)
|
||||
else:
|
||||
print('%d subtitles downloaded' % subtitles_count)
|
||||
if p.discarded_providers:
|
||||
click.secho('Some providers have been discarded due to unexpected errors: %s' %
|
||||
', '.join(p.discarded_providers), fg='yellow')
|
||||
|
||||
# save subtitles
|
||||
total_subtitles = 0
|
||||
for v, subtitles in downloaded_subtitles.items():
|
||||
saved_subtitles = save_subtitles(v, subtitles, single=single, directory=directory, encoding=encoding)
|
||||
total_subtitles += len(saved_subtitles)
|
||||
|
||||
if verbose > 0:
|
||||
click.echo('%s subtitle%s downloaded for %s' % (click.style(str(len(saved_subtitles)), bold=True),
|
||||
's' if len(saved_subtitles) > 1 else '',
|
||||
os.path.split(v.name)[1]))
|
||||
|
||||
if verbose > 1:
|
||||
for s in saved_subtitles:
|
||||
matches = s.get_matches(v)
|
||||
score = compute_score(s, v)
|
||||
|
||||
# score color
|
||||
score_color = None
|
||||
scores = get_scores(v)
|
||||
if isinstance(v, Movie):
|
||||
if score < scores['title']:
|
||||
score_color = 'red'
|
||||
elif score < scores['title'] + scores['year'] + scores['release_group']:
|
||||
score_color = 'yellow'
|
||||
else:
|
||||
score_color = 'green'
|
||||
elif isinstance(v, Episode):
|
||||
if score < scores['series'] + scores['season'] + scores['episode']:
|
||||
score_color = 'red'
|
||||
elif score < scores['series'] + scores['season'] + scores['episode'] + scores['release_group']:
|
||||
score_color = 'yellow'
|
||||
else:
|
||||
score_color = 'green'
|
||||
|
||||
# scale score from 0 to 100 taking out preferences
|
||||
scaled_score = score
|
||||
if s.hearing_impaired == hearing_impaired:
|
||||
scaled_score -= scores['hearing_impaired']
|
||||
scaled_score *= 100 / scores['hash']
|
||||
|
||||
# echo some nice colored output
|
||||
click.echo(' - [{score}] {language} subtitle from {provider_name} (match on {matches})'.format(
|
||||
score=click.style('{:5.1f}'.format(scaled_score), fg=score_color, bold=score >= scores['hash']),
|
||||
language=s.language.name if s.language.country is None else '%s (%s)' % (s.language.name,
|
||||
s.language.country.name),
|
||||
provider_name=s.provider_name,
|
||||
matches=', '.join(sorted(matches, key=scores.get, reverse=True))
|
||||
))
|
||||
|
||||
if verbose == 0:
|
||||
click.echo('Downloaded %s subtitle%s' % (click.style(str(total_subtitles), bold=True),
|
||||
's' if total_subtitles > 1 else ''))
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
from babelfish import LanguageReverseConverter, get_language_converter
|
||||
from babelfish import LanguageReverseConverter, language_converters
|
||||
|
||||
|
||||
class Addic7edConverter(LanguageReverseConverter):
|
||||
def __init__(self):
|
||||
self.name_converter = get_language_converter('name')
|
||||
self.from_addic7ed = {'Català': ('cat',), 'Chinese (Simplified)': ('zho',), 'Chinese (Traditional)': ('zho',),
|
||||
self.name_converter = language_converters['name']
|
||||
self.from_addic7ed = {u'Català': ('cat',), 'Chinese (Simplified)': ('zho',), 'Chinese (Traditional)': ('zho',),
|
||||
'Euskera': ('eus',), 'Galego': ('glg',), 'Greek': ('ell',), 'Malay': ('msa',),
|
||||
'Portuguese (Brazilian)': ('por', 'BR'), 'Serbian (Cyrillic)': ('srp', None, 'Cyrl'),
|
||||
'Serbian (Latin)': ('srp',), 'Spanish (Latin America)': ('spa',),
|
||||
@@ -23,9 +22,11 @@ class Addic7edConverter(LanguageReverseConverter):
|
||||
return self.to_addic7ed[(alpha3, country)]
|
||||
if (alpha3,) in self.to_addic7ed:
|
||||
return self.to_addic7ed[(alpha3,)]
|
||||
|
||||
return self.name_converter.convert(alpha3, country, script)
|
||||
|
||||
def reverse(self, addic7ed):
|
||||
if addic7ed in self.from_addic7ed:
|
||||
return self.from_addic7ed[addic7ed]
|
||||
|
||||
return self.name_converter.reverse(addic7ed)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from babelfish import LanguageReverseConverter
|
||||
|
||||
from ..exceptions import ConfigurationError
|
||||
|
||||
|
||||
class LegendasTVConverter(LanguageReverseConverter):
|
||||
def __init__(self):
|
||||
self.from_legendastv = {1: ('por', 'BR'), 2: ('eng',), 3: ('spa',), 4: ('fra',), 5: ('deu',), 6: ('jpn',),
|
||||
7: ('dan',), 8: ('nor',), 9: ('swe',), 10: ('por',), 11: ('ara',), 12: ('ces',),
|
||||
13: ('zho',), 14: ('kor',), 15: ('bul',), 16: ('ita',), 17: ('pol',)}
|
||||
self.to_legendastv = {v: k for k, v in self.from_legendastv.items()}
|
||||
self.codes = set(self.from_legendastv.keys())
|
||||
|
||||
def convert(self, alpha3, country=None, script=None):
|
||||
if (alpha3, country) in self.to_legendastv:
|
||||
return self.to_legendastv[(alpha3, country)]
|
||||
if (alpha3,) in self.to_legendastv:
|
||||
return self.to_legendastv[(alpha3,)]
|
||||
|
||||
raise ConfigurationError('Unsupported language code for legendastv: %s, %s, %s' % (alpha3, country, script))
|
||||
|
||||
def reverse(self, legendastv):
|
||||
if legendastv in self.from_legendastv:
|
||||
return self.from_legendastv[legendastv]
|
||||
|
||||
raise ConfigurationError('Unsupported language number for legendastv: %s' % legendastv)
|
||||
@@ -1,32 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
from babelfish import LanguageReverseConverter, LanguageConvertError, LanguageReverseError
|
||||
|
||||
|
||||
class PodnapisiConverter(LanguageReverseConverter):
|
||||
def __init__(self):
|
||||
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 LanguageConvertError(alpha3, country, script)
|
||||
|
||||
def reverse(self, podnapisi):
|
||||
if podnapisi not in self.from_podnapisi:
|
||||
raise LanguageReverseError(podnapisi)
|
||||
return self.from_podnapisi[podnapisi]
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from babelfish import LanguageReverseConverter
|
||||
|
||||
from ..exceptions import ConfigurationError
|
||||
|
||||
|
||||
class ShooterConverter(LanguageReverseConverter):
|
||||
def __init__(self):
|
||||
self.from_shooter = {'chn': ('zho',), 'eng': ('eng',)}
|
||||
self.to_shooter = {v: k for k, v in self.from_shooter.items()}
|
||||
self.codes = set(self.from_shooter.keys())
|
||||
|
||||
def convert(self, alpha3, country=None, script=None):
|
||||
if (alpha3,) in self.to_shooter:
|
||||
return self.to_shooter[(alpha3,)]
|
||||
|
||||
raise ConfigurationError('Unsupported language for shooter: %s, %s, %s' % (alpha3, country, script))
|
||||
|
||||
def reverse(self, shooter):
|
||||
if shooter in self.from_shooter:
|
||||
return self.from_shooter[shooter]
|
||||
|
||||
raise ConfigurationError('Unsupported language code for shooter: %s' % shooter)
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from babelfish import LanguageReverseConverter
|
||||
|
||||
from ..exceptions import ConfigurationError
|
||||
|
||||
|
||||
class TheSubDBConverter(LanguageReverseConverter):
|
||||
def __init__(self):
|
||||
self.from_thesubdb = {'en': ('eng',), 'es': ('spa',), 'fr': ('fra',), 'it': ('ita',), 'nl': ('nld',),
|
||||
'pl': ('pol',), 'pt': ('por', 'BR'), 'ro': ('ron',), 'sv': ('swe',), 'tr': ('tur',)}
|
||||
self.to_thesubdb = {v: k for k, v in self.from_thesubdb.items()}
|
||||
self.codes = set(self.from_thesubdb.keys())
|
||||
|
||||
def convert(self, alpha3, country=None, script=None):
|
||||
if (alpha3, country) in self.to_thesubdb:
|
||||
return self.to_thesubdb[(alpha3, country)]
|
||||
if (alpha3,) in self.to_thesubdb:
|
||||
return self.to_thesubdb[(alpha3,)]
|
||||
|
||||
raise ConfigurationError('Unsupported language for thesubdb: %s, %s, %s' % (alpha3, country, script))
|
||||
|
||||
def reverse(self, thesubdb):
|
||||
if thesubdb in self.from_thesubdb:
|
||||
return self.from_thesubdb[thesubdb]
|
||||
|
||||
raise ConfigurationError('Unsupported language code for thesubdb: %s' % thesubdb)
|
||||
@@ -1,14 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
from babelfish import LanguageReverseConverter, get_language_converter
|
||||
from babelfish import LanguageReverseConverter, language_converters
|
||||
|
||||
|
||||
class TVsubtitlesConverter(LanguageReverseConverter):
|
||||
def __init__(self):
|
||||
self.alpha2_converter = get_language_converter('alpha2')
|
||||
self.alpha2_converter = language_converters['alpha2']
|
||||
self.from_tvsubtitles = {'br': ('por', 'BR'), 'ua': ('ukr',), 'gr': ('ell',), 'cn': ('zho',), 'jp': ('jpn',),
|
||||
'cz': ('ces',)}
|
||||
self.to_tvsubtitles = {v: k for k, v in self.from_tvsubtitles}
|
||||
self.to_tvsubtitles = {v: k for k, v in self.from_tvsubtitles.items()}
|
||||
self.codes = self.alpha2_converter.codes | set(self.from_tvsubtitles.keys())
|
||||
|
||||
def convert(self, alpha3, country=None, script=None):
|
||||
@@ -16,9 +15,11 @@ class TVsubtitlesConverter(LanguageReverseConverter):
|
||||
return self.to_tvsubtitles[(alpha3, country)]
|
||||
if (alpha3,) in self.to_tvsubtitles:
|
||||
return self.to_tvsubtitles[(alpha3,)]
|
||||
|
||||
return self.alpha2_converter.convert(alpha3, country, script)
|
||||
|
||||
def reverse(self, tvsubtitles):
|
||||
if tvsubtitles in self.from_tvsubtitles:
|
||||
return self.from_tvsubtitles[tvsubtitles]
|
||||
|
||||
return self.alpha2_converter.reverse(tvsubtitles)
|
||||
|
||||
@@ -0,0 +1,705 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from collections import defaultdict
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
import io
|
||||
import itertools
|
||||
import logging
|
||||
import operator
|
||||
import os.path
|
||||
import socket
|
||||
|
||||
from babelfish import Language, LanguageReverseError
|
||||
from guessit import guessit
|
||||
from rarfile import NotRarFile, RarCannotExec, RarFile
|
||||
import requests
|
||||
|
||||
from .extensions import provider_manager, refiner_manager
|
||||
from .score import compute_score as default_compute_score
|
||||
from .subtitle import SUBTITLE_EXTENSIONS, get_subtitle_path
|
||||
from .utils import hash_napiprojekt, hash_opensubtitles, hash_shooter, hash_thesubdb
|
||||
from .video import VIDEO_EXTENSIONS, Episode, Movie, Video
|
||||
|
||||
#: Supported archive extensions
|
||||
ARCHIVE_EXTENSIONS = ('.rar',)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProviderPool(object):
|
||||
"""A pool of providers with the same API as a single :class:`~subliminal.providers.Provider`.
|
||||
|
||||
It has a few extra features:
|
||||
|
||||
* Lazy loads providers when needed and supports the `with` statement to :meth:`terminate`
|
||||
the providers on exit.
|
||||
* Automatically discard providers on failure.
|
||||
|
||||
:param list providers: name of providers to use, if not all.
|
||||
:param dict provider_configs: provider configuration as keyword arguments per provider name to pass when
|
||||
instanciating the :class:`~subliminal.providers.Provider`.
|
||||
|
||||
"""
|
||||
def __init__(self, providers=None, provider_configs=None):
|
||||
#: Name of providers to use
|
||||
self.providers = providers or provider_manager.names()
|
||||
|
||||
#: Provider configuration
|
||||
self.provider_configs = provider_configs or {}
|
||||
|
||||
#: Initialized providers
|
||||
self.initialized_providers = {}
|
||||
|
||||
#: Discarded providers
|
||||
self.discarded_providers = set()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.terminate()
|
||||
|
||||
def __getitem__(self, name):
|
||||
if name not in self.providers:
|
||||
raise KeyError
|
||||
if name not in self.initialized_providers:
|
||||
logger.info('Initializing provider %s', name)
|
||||
provider = provider_manager[name].plugin(**self.provider_configs.get(name, {}))
|
||||
provider.initialize()
|
||||
self.initialized_providers[name] = provider
|
||||
|
||||
return self.initialized_providers[name]
|
||||
|
||||
def __delitem__(self, name):
|
||||
if name not in self.initialized_providers:
|
||||
raise KeyError(name)
|
||||
|
||||
try:
|
||||
logger.info('Terminating provider %s', name)
|
||||
self.initialized_providers[name].terminate()
|
||||
except (requests.Timeout, socket.timeout):
|
||||
logger.error('Provider %r timed out, improperly terminated', name)
|
||||
except:
|
||||
logger.exception('Provider %r terminated unexpectedly', name)
|
||||
|
||||
del self.initialized_providers[name]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.initialized_providers)
|
||||
|
||||
def list_subtitles_provider(self, provider, video, languages):
|
||||
"""List subtitles with a single provider.
|
||||
|
||||
The video and languages are checked against the provider.
|
||||
|
||||
:param str provider: name of the provider.
|
||||
:param video: video to list subtitles for.
|
||||
:type video: :class:`~subliminal.video.Video`
|
||||
:param languages: languages to search for.
|
||||
:type languages: set of :class:`~babelfish.language.Language`
|
||||
:return: found subtitles.
|
||||
:rtype: list of :class:`~subliminal.subtitle.Subtitle` or None
|
||||
|
||||
"""
|
||||
# check video validity
|
||||
if not provider_manager[provider].plugin.check(video):
|
||||
logger.info('Skipping provider %r: not a valid video', provider)
|
||||
return []
|
||||
|
||||
# check supported languages
|
||||
provider_languages = provider_manager[provider].plugin.languages & languages
|
||||
if not provider_languages:
|
||||
logger.info('Skipping provider %r: no language to search for', provider)
|
||||
return []
|
||||
|
||||
# list subtitles
|
||||
logger.info('Listing subtitles with provider %r and languages %r', provider, provider_languages)
|
||||
try:
|
||||
return self[provider].list_subtitles(video, provider_languages)
|
||||
except (requests.Timeout, socket.timeout):
|
||||
logger.error('Provider %r timed out', provider)
|
||||
except:
|
||||
logger.exception('Unexpected error in provider %r', provider)
|
||||
|
||||
def list_subtitles(self, video, languages):
|
||||
"""List subtitles.
|
||||
|
||||
:param video: video to list subtitles for.
|
||||
:type video: :class:`~subliminal.video.Video`
|
||||
:param languages: languages to search for.
|
||||
:type languages: set of :class:`~babelfish.language.Language`
|
||||
:return: found subtitles.
|
||||
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
|
||||
|
||||
"""
|
||||
subtitles = []
|
||||
|
||||
for name in self.providers:
|
||||
# check discarded providers
|
||||
if name in self.discarded_providers:
|
||||
logger.debug('Skipping discarded provider %r', name)
|
||||
continue
|
||||
|
||||
# list subtitles
|
||||
provider_subtitles = self.list_subtitles_provider(name, video, languages)
|
||||
if provider_subtitles is None:
|
||||
logger.info('Discarding provider %s', name)
|
||||
self.discarded_providers.add(name)
|
||||
continue
|
||||
|
||||
# add the subtitles
|
||||
subtitles.extend(provider_subtitles)
|
||||
|
||||
return subtitles
|
||||
|
||||
def download_subtitle(self, subtitle):
|
||||
"""Download `subtitle`'s :attr:`~subliminal.subtitle.Subtitle.content`.
|
||||
|
||||
:param subtitle: subtitle to download.
|
||||
:type subtitle: :class:`~subliminal.subtitle.Subtitle`
|
||||
:return: `True` if the subtitle has been successfully downloaded, `False` otherwise.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
# check discarded providers
|
||||
if subtitle.provider_name in self.discarded_providers:
|
||||
logger.warning('Provider %r is discarded', subtitle.provider_name)
|
||||
return False
|
||||
|
||||
logger.info('Downloading subtitle %r', subtitle)
|
||||
try:
|
||||
self[subtitle.provider_name].download_subtitle(subtitle)
|
||||
except (requests.Timeout, socket.timeout):
|
||||
logger.error('Provider %r timed out, discarding it', subtitle.provider_name)
|
||||
self.discarded_providers.add(subtitle.provider_name)
|
||||
return False
|
||||
except:
|
||||
logger.exception('Unexpected error in provider %r, discarding it', subtitle.provider_name)
|
||||
self.discarded_providers.add(subtitle.provider_name)
|
||||
return False
|
||||
|
||||
# check subtitle validity
|
||||
if not subtitle.is_valid():
|
||||
logger.error('Invalid subtitle')
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def download_best_subtitles(self, subtitles, video, languages, min_score=0, hearing_impaired=False, only_one=False,
|
||||
compute_score=None):
|
||||
"""Download the best matching subtitles.
|
||||
|
||||
:param subtitles: the subtitles to use.
|
||||
:type subtitles: list of :class:`~subliminal.subtitle.Subtitle`
|
||||
:param video: video to download subtitles for.
|
||||
:type video: :class:`~subliminal.video.Video`
|
||||
:param languages: languages to download.
|
||||
:type languages: set of :class:`~babelfish.language.Language`
|
||||
:param int min_score: minimum score for a subtitle to be downloaded.
|
||||
:param bool hearing_impaired: hearing impaired preference.
|
||||
:param bool only_one: download only one subtitle, not one per language.
|
||||
:param compute_score: function that takes `subtitle` and `video` as positional arguments,
|
||||
`hearing_impaired` as keyword argument and returns the score.
|
||||
:return: downloaded subtitles.
|
||||
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
|
||||
|
||||
"""
|
||||
compute_score = compute_score or default_compute_score
|
||||
|
||||
# sort subtitles by score
|
||||
scored_subtitles = sorted([(s, compute_score(s, video, hearing_impaired=hearing_impaired))
|
||||
for s in subtitles], key=operator.itemgetter(1), reverse=True)
|
||||
|
||||
# download best subtitles, falling back on the next on error
|
||||
downloaded_subtitles = []
|
||||
for subtitle, score in scored_subtitles:
|
||||
# check score
|
||||
if score < min_score:
|
||||
logger.info('Score %d is below min_score (%d)', score, min_score)
|
||||
break
|
||||
|
||||
# check downloaded languages
|
||||
if subtitle.language in set(s.language for s in downloaded_subtitles):
|
||||
logger.debug('Skipping subtitle: %r already downloaded', subtitle.language)
|
||||
continue
|
||||
|
||||
# download
|
||||
if self.download_subtitle(subtitle):
|
||||
downloaded_subtitles.append(subtitle)
|
||||
|
||||
# stop when all languages are downloaded
|
||||
if set(s.language for s in downloaded_subtitles) == languages:
|
||||
logger.debug('All languages downloaded')
|
||||
break
|
||||
|
||||
# stop if only one subtitle is requested
|
||||
if only_one:
|
||||
logger.debug('Only one subtitle downloaded')
|
||||
break
|
||||
|
||||
return downloaded_subtitles
|
||||
|
||||
def terminate(self):
|
||||
"""Terminate all the :attr:`initialized_providers`."""
|
||||
logger.debug('Terminating initialized providers')
|
||||
for name in list(self.initialized_providers):
|
||||
del self[name]
|
||||
|
||||
|
||||
class AsyncProviderPool(ProviderPool):
|
||||
"""Subclass of :class:`ProviderPool` with asynchronous support for :meth:`~ProviderPool.list_subtitles`.
|
||||
|
||||
:param int max_workers: maximum number of threads to use. If `None`, :attr:`max_workers` will be set
|
||||
to the number of :attr:`~ProviderPool.providers`.
|
||||
|
||||
"""
|
||||
def __init__(self, max_workers=None, *args, **kwargs):
|
||||
super(AsyncProviderPool, self).__init__(*args, **kwargs)
|
||||
|
||||
#: Maximum number of threads to use
|
||||
self.max_workers = max_workers or len(self.providers)
|
||||
|
||||
def list_subtitles_provider(self, provider, video, languages):
|
||||
return provider, super(AsyncProviderPool, self).list_subtitles_provider(provider, video, languages)
|
||||
|
||||
def list_subtitles(self, video, languages):
|
||||
subtitles = []
|
||||
|
||||
with ThreadPoolExecutor(self.max_workers) as executor:
|
||||
for provider, provider_subtitles in executor.map(self.list_subtitles_provider, self.providers,
|
||||
itertools.repeat(video, len(self.providers)),
|
||||
itertools.repeat(languages, len(self.providers))):
|
||||
# discard provider that failed
|
||||
if provider_subtitles is None:
|
||||
logger.info('Discarding provider %s', provider)
|
||||
self.discarded_providers.add(provider)
|
||||
continue
|
||||
|
||||
# add subtitles
|
||||
subtitles.extend(provider_subtitles)
|
||||
|
||||
return subtitles
|
||||
|
||||
|
||||
def check_video(video, languages=None, age=None, undefined=False):
|
||||
"""Perform some checks on the `video`.
|
||||
|
||||
All the checks are optional. Return `False` if any of this check fails:
|
||||
|
||||
* `languages` already exist in `video`'s :attr:`~subliminal.video.Video.subtitle_languages`.
|
||||
* `video` is older than `age`.
|
||||
* `video` has an `undefined` language in :attr:`~subliminal.video.Video.subtitle_languages`.
|
||||
|
||||
:param video: video to check.
|
||||
:type video: :class:`~subliminal.video.Video`
|
||||
:param languages: desired languages.
|
||||
:type languages: set of :class:`~babelfish.language.Language`
|
||||
:param datetime.timedelta age: maximum age of the video.
|
||||
:param bool undefined: fail on existing undefined language.
|
||||
:return: `True` if the video passes the checks, `False` otherwise.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
# language test
|
||||
if languages and not (languages - video.subtitle_languages):
|
||||
logger.debug('All languages %r exist', languages)
|
||||
return False
|
||||
|
||||
# age test
|
||||
if age and video.age > age:
|
||||
logger.debug('Video is older than %r', age)
|
||||
return False
|
||||
|
||||
# undefined test
|
||||
if undefined and Language('und') in video.subtitle_languages:
|
||||
logger.debug('Undefined language found')
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def search_external_subtitles(path, directory=None):
|
||||
"""Search for external subtitles from a video `path` and their associated language.
|
||||
|
||||
Unless `directory` is provided, search will be made in the same directory as the video file.
|
||||
|
||||
:param str path: path to the video.
|
||||
:param str directory: directory to search for subtitles.
|
||||
:return: found subtitles with their languages.
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
# split path
|
||||
dirpath, filename = os.path.split(path)
|
||||
dirpath = dirpath or '.'
|
||||
fileroot, fileext = os.path.splitext(filename)
|
||||
|
||||
# search for subtitles
|
||||
subtitles = {}
|
||||
for p in os.listdir(directory or dirpath):
|
||||
# keep only valid subtitle filenames
|
||||
if not p.startswith(fileroot) or not p.endswith(SUBTITLE_EXTENSIONS):
|
||||
continue
|
||||
|
||||
# extract the potential language code
|
||||
language = Language('und')
|
||||
language_code = p[len(fileroot):-len(os.path.splitext(p)[1])].replace(fileext, '').replace('_', '-')[1:]
|
||||
if language_code:
|
||||
try:
|
||||
language = Language.fromietf(language_code)
|
||||
except (ValueError, LanguageReverseError):
|
||||
logger.error('Cannot parse language code %r', language_code)
|
||||
|
||||
subtitles[p] = language
|
||||
|
||||
logger.debug('Found subtitles %r', subtitles)
|
||||
|
||||
return subtitles
|
||||
|
||||
|
||||
def scan_video(path):
|
||||
"""Scan a video from a `path`.
|
||||
|
||||
:param str path: existing path to the video.
|
||||
:return: the scanned video.
|
||||
:rtype: :class:`~subliminal.video.Video`
|
||||
|
||||
"""
|
||||
# check for non-existing path
|
||||
if not os.path.exists(path):
|
||||
raise ValueError('Path does not exist')
|
||||
|
||||
# check video extension
|
||||
if not path.endswith(VIDEO_EXTENSIONS):
|
||||
raise ValueError('%r is not a valid video extension' % os.path.splitext(path)[1])
|
||||
|
||||
dirpath, filename = os.path.split(path)
|
||||
logger.info('Scanning video %r in %r', filename, dirpath)
|
||||
|
||||
# guess
|
||||
video = Video.fromguess(path, guessit(path))
|
||||
|
||||
# size and hashes
|
||||
video.size = os.path.getsize(path)
|
||||
if video.size > 10485760:
|
||||
logger.debug('Size is %d', video.size)
|
||||
video.hashes['opensubtitles'] = hash_opensubtitles(path)
|
||||
video.hashes['shooter'] = hash_shooter(path)
|
||||
video.hashes['thesubdb'] = hash_thesubdb(path)
|
||||
video.hashes['napiprojekt'] = hash_napiprojekt(path)
|
||||
logger.debug('Computed hashes %r', video.hashes)
|
||||
else:
|
||||
logger.warning('Size is lower than 10MB: hashes not computed')
|
||||
|
||||
return video
|
||||
|
||||
|
||||
def scan_archive(path):
|
||||
"""Scan an archive from a `path`.
|
||||
|
||||
:param str path: existing path to the archive.
|
||||
:return: the scanned video.
|
||||
:rtype: :class:`~subliminal.video.Video`
|
||||
|
||||
"""
|
||||
# check for non-existing path
|
||||
if not os.path.exists(path):
|
||||
raise ValueError('Path does not exist')
|
||||
|
||||
# check video extension
|
||||
if not path.endswith(ARCHIVE_EXTENSIONS):
|
||||
raise ValueError('%r is not a valid archive extension' % os.path.splitext(path)[1])
|
||||
|
||||
dirpath, filename = os.path.split(path)
|
||||
logger.info('Scanning archive %r in %r', filename, dirpath)
|
||||
|
||||
# rar extension
|
||||
if filename.endswith('.rar'):
|
||||
rar = RarFile(path)
|
||||
|
||||
# filter on video extensions
|
||||
rar_filenames = [f for f in rar.namelist() if f.endswith(VIDEO_EXTENSIONS)]
|
||||
|
||||
# no video found
|
||||
if not rar_filenames:
|
||||
raise ValueError('No video in archive')
|
||||
|
||||
# more than one video found
|
||||
if len(rar_filenames) > 1:
|
||||
raise ValueError('More than one video in archive')
|
||||
|
||||
# guess
|
||||
rar_filename = rar_filenames[0]
|
||||
rar_filepath = os.path.join(dirpath, rar_filename)
|
||||
video = Video.fromguess(rar_filepath, guessit(rar_filepath))
|
||||
|
||||
# size
|
||||
video.size = rar.getinfo(rar_filename).file_size
|
||||
else:
|
||||
raise ValueError('Unsupported extension %r' % os.path.splitext(path)[1])
|
||||
|
||||
return video
|
||||
|
||||
|
||||
def scan_videos(path, age=None, archives=True):
|
||||
"""Scan `path` for videos and their subtitles.
|
||||
|
||||
See :func:`refine` to find additional information for the video.
|
||||
|
||||
:param str path: existing directory path to scan.
|
||||
:param datetime.timedelta age: maximum age of the video or archive.
|
||||
:param bool archives: scan videos in archives.
|
||||
:return: the scanned videos.
|
||||
:rtype: list of :class:`~subliminal.video.Video`
|
||||
|
||||
"""
|
||||
# check for non-existing path
|
||||
if not os.path.exists(path):
|
||||
raise ValueError('Path does not exist')
|
||||
|
||||
# check for non-directory path
|
||||
if not os.path.isdir(path):
|
||||
raise ValueError('Path is not a directory')
|
||||
|
||||
# walk the path
|
||||
videos = []
|
||||
for dirpath, dirnames, filenames in os.walk(path):
|
||||
logger.debug('Walking directory %r', dirpath)
|
||||
|
||||
# remove badly encoded and hidden dirnames
|
||||
for dirname in list(dirnames):
|
||||
if dirname.startswith('.'):
|
||||
logger.debug('Skipping hidden dirname %r in %r', dirname, dirpath)
|
||||
dirnames.remove(dirname)
|
||||
|
||||
# scan for videos
|
||||
for filename in filenames:
|
||||
# filter on videos and archives
|
||||
if not (filename.endswith(VIDEO_EXTENSIONS) or archives and filename.endswith(ARCHIVE_EXTENSIONS)):
|
||||
continue
|
||||
|
||||
# skip hidden files
|
||||
if filename.startswith('.'):
|
||||
logger.debug('Skipping hidden filename %r in %r', filename, dirpath)
|
||||
continue
|
||||
|
||||
# reconstruct the file path
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
|
||||
# skip links
|
||||
if os.path.islink(filepath):
|
||||
logger.debug('Skipping link %r in %r', filename, dirpath)
|
||||
continue
|
||||
|
||||
# skip old files
|
||||
if age and datetime.utcnow() - datetime.utcfromtimestamp(os.path.getmtime(filepath)) > age:
|
||||
logger.debug('Skipping old file %r in %r', filename, dirpath)
|
||||
continue
|
||||
|
||||
# scan
|
||||
if filename.endswith(VIDEO_EXTENSIONS): # video
|
||||
try:
|
||||
video = scan_video(filepath)
|
||||
except ValueError: # pragma: no cover
|
||||
logger.exception('Error scanning video')
|
||||
continue
|
||||
elif archives and filename.endswith(ARCHIVE_EXTENSIONS): # archive
|
||||
try:
|
||||
video = scan_archive(filepath)
|
||||
except (NotRarFile, RarCannotExec, ValueError): # pragma: no cover
|
||||
logger.exception('Error scanning archive')
|
||||
continue
|
||||
else: # pragma: no cover
|
||||
raise ValueError('Unsupported file %r' % filename)
|
||||
|
||||
videos.append(video)
|
||||
|
||||
return videos
|
||||
|
||||
|
||||
def refine(video, episode_refiners=None, movie_refiners=None, **kwargs):
|
||||
"""Refine a video using :ref:`refiners`.
|
||||
|
||||
.. note::
|
||||
|
||||
Exceptions raised in refiners are silently passed and logged.
|
||||
|
||||
:param video: the video to refine.
|
||||
:type video: :class:`~subliminal.video.Video`
|
||||
:param tuple episode_refiners: refiners to use for episodes.
|
||||
:param tuple movie_refiners: refiners to use for movies.
|
||||
:param \*\*kwargs: additional parameters for the :func:`~subliminal.refiners.refine` functions.
|
||||
|
||||
"""
|
||||
refiners = ()
|
||||
if isinstance(video, Episode):
|
||||
refiners = episode_refiners or ('metadata', 'tvdb', 'omdb')
|
||||
elif isinstance(video, Movie):
|
||||
refiners = movie_refiners or ('metadata', 'omdb')
|
||||
for refiner in refiners:
|
||||
logger.info('Refining video with %s', refiner)
|
||||
try:
|
||||
refiner_manager[refiner].plugin(video, **kwargs)
|
||||
except:
|
||||
logger.exception('Failed to refine video')
|
||||
|
||||
|
||||
def list_subtitles(videos, languages, pool_class=ProviderPool, **kwargs):
|
||||
"""List subtitles.
|
||||
|
||||
The `videos` must pass the `languages` check of :func:`check_video`.
|
||||
|
||||
:param videos: videos to list subtitles for.
|
||||
:type videos: set of :class:`~subliminal.video.Video`
|
||||
:param languages: languages to search for.
|
||||
:type languages: set of :class:`~babelfish.language.Language`
|
||||
:param pool_class: class to use as provider pool.
|
||||
:type pool_class: :class:`ProviderPool`, :class:`AsyncProviderPool` or similar
|
||||
:param \*\*kwargs: additional parameters for the provided `pool_class` constructor.
|
||||
:return: found subtitles per video.
|
||||
:rtype: dict of :class:`~subliminal.video.Video` to list of :class:`~subliminal.subtitle.Subtitle`
|
||||
|
||||
"""
|
||||
listed_subtitles = defaultdict(list)
|
||||
|
||||
# check videos
|
||||
checked_videos = []
|
||||
for video in videos:
|
||||
if not check_video(video, languages=languages):
|
||||
logger.info('Skipping video %r', video)
|
||||
continue
|
||||
checked_videos.append(video)
|
||||
|
||||
# return immediately if no video passed the checks
|
||||
if not checked_videos:
|
||||
return listed_subtitles
|
||||
|
||||
# list subtitles
|
||||
with pool_class(**kwargs) as pool:
|
||||
for video in checked_videos:
|
||||
logger.info('Listing subtitles for %r', video)
|
||||
subtitles = pool.list_subtitles(video, languages - video.subtitle_languages)
|
||||
listed_subtitles[video].extend(subtitles)
|
||||
logger.info('Found %d subtitle(s)', len(subtitles))
|
||||
|
||||
return listed_subtitles
|
||||
|
||||
|
||||
def download_subtitles(subtitles, pool_class=ProviderPool, **kwargs):
|
||||
"""Download :attr:`~subliminal.subtitle.Subtitle.content` of `subtitles`.
|
||||
|
||||
:param subtitles: subtitles to download.
|
||||
:type subtitles: list of :class:`~subliminal.subtitle.Subtitle`
|
||||
:param pool_class: class to use as provider pool.
|
||||
:type pool_class: :class:`ProviderPool`, :class:`AsyncProviderPool` or similar
|
||||
:param \*\*kwargs: additional parameters for the provided `pool_class` constructor.
|
||||
|
||||
"""
|
||||
with pool_class(**kwargs) as pool:
|
||||
for subtitle in subtitles:
|
||||
logger.info('Downloading subtitle %r', subtitle)
|
||||
pool.download_subtitle(subtitle)
|
||||
|
||||
|
||||
def download_best_subtitles(videos, languages, min_score=0, hearing_impaired=False, only_one=False, compute_score=None,
|
||||
pool_class=ProviderPool, **kwargs):
|
||||
"""List and download the best matching subtitles.
|
||||
|
||||
The `videos` must pass the `languages` and `undefined` (`only_one`) checks of :func:`check_video`.
|
||||
|
||||
:param videos: videos to download subtitles for.
|
||||
:type videos: set of :class:`~subliminal.video.Video`
|
||||
:param languages: languages to download.
|
||||
:type languages: set of :class:`~babelfish.language.Language`
|
||||
:param int min_score: minimum score for a subtitle to be downloaded.
|
||||
:param bool hearing_impaired: hearing impaired preference.
|
||||
:param bool only_one: download only one subtitle, not one per language.
|
||||
:param compute_score: function that takes `subtitle` and `video` as positional arguments,
|
||||
`hearing_impaired` as keyword argument and returns the score.
|
||||
:param pool_class: class to use as provider pool.
|
||||
:type pool_class: :class:`ProviderPool`, :class:`AsyncProviderPool` or similar
|
||||
:param \*\*kwargs: additional parameters for the provided `pool_class` constructor.
|
||||
:return: downloaded subtitles per video.
|
||||
:rtype: dict of :class:`~subliminal.video.Video` to list of :class:`~subliminal.subtitle.Subtitle`
|
||||
|
||||
"""
|
||||
downloaded_subtitles = defaultdict(list)
|
||||
|
||||
# check videos
|
||||
checked_videos = []
|
||||
for video in videos:
|
||||
if not check_video(video, languages=languages, undefined=only_one):
|
||||
logger.info('Skipping video %r', video)
|
||||
continue
|
||||
checked_videos.append(video)
|
||||
|
||||
# return immediately if no video passed the checks
|
||||
if not checked_videos:
|
||||
return downloaded_subtitles
|
||||
|
||||
# download best subtitles
|
||||
with pool_class(**kwargs) as pool:
|
||||
for video in checked_videos:
|
||||
logger.info('Downloading best subtitles for %r', video)
|
||||
subtitles = pool.download_best_subtitles(pool.list_subtitles(video, languages - video.subtitle_languages),
|
||||
video, languages, min_score=min_score,
|
||||
hearing_impaired=hearing_impaired, only_one=only_one,
|
||||
compute_score=compute_score)
|
||||
logger.info('Downloaded %d subtitle(s)', len(subtitles))
|
||||
downloaded_subtitles[video].extend(subtitles)
|
||||
|
||||
return downloaded_subtitles
|
||||
|
||||
|
||||
def save_subtitles(video, subtitles, single=False, directory=None, encoding=None):
|
||||
"""Save subtitles on filesystem.
|
||||
|
||||
Subtitles are saved in the order of the list. If a subtitle with a language has already been saved, other subtitles
|
||||
with the same language are silently ignored.
|
||||
|
||||
The extension used is `.lang.srt` by default or `.srt` is `single` is `True`, with `lang` being the IETF code for
|
||||
the :attr:`~subliminal.subtitle.Subtitle.language` of the subtitle.
|
||||
|
||||
:param video: video of the subtitles.
|
||||
:type video: :class:`~subliminal.video.Video`
|
||||
:param subtitles: subtitles to save.
|
||||
:type subtitles: list of :class:`~subliminal.subtitle.Subtitle`
|
||||
:param bool single: save a single subtitle, default is to save one subtitle per language.
|
||||
:param str directory: path to directory where to save the subtitles, default is next to the video.
|
||||
:param str encoding: encoding in which to save the subtitles, default is to keep original encoding.
|
||||
:return: the saved subtitles
|
||||
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
|
||||
|
||||
"""
|
||||
saved_subtitles = []
|
||||
for subtitle in subtitles:
|
||||
# check content
|
||||
if subtitle.content is None:
|
||||
logger.error('Skipping subtitle %r: no content', subtitle)
|
||||
continue
|
||||
|
||||
# check language
|
||||
if subtitle.language in set(s.language for s in saved_subtitles):
|
||||
logger.debug('Skipping subtitle %r: language already saved', subtitle)
|
||||
continue
|
||||
|
||||
# create subtitle path
|
||||
subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language)
|
||||
if directory is not None:
|
||||
subtitle_path = os.path.join(directory, os.path.split(subtitle_path)[1])
|
||||
|
||||
# save content as is or in the specified encoding
|
||||
logger.info('Saving %r to %r', subtitle, subtitle_path)
|
||||
if encoding is None:
|
||||
with io.open(subtitle_path, 'wb') as f:
|
||||
f.write(subtitle.content)
|
||||
else:
|
||||
with io.open(subtitle_path, 'w', encoding=encoding) as f:
|
||||
f.write(subtitle.text)
|
||||
saved_subtitles.append(subtitle)
|
||||
|
||||
# check single
|
||||
if single:
|
||||
break
|
||||
|
||||
return saved_subtitles
|
||||
+13
-11
@@ -1,27 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Base class for exceptions in subliminal"""
|
||||
"""Base class for exceptions in subliminal."""
|
||||
pass
|
||||
|
||||
|
||||
class ProviderError(Error):
|
||||
"""Exception raised by providers"""
|
||||
"""Exception raised by providers."""
|
||||
pass
|
||||
|
||||
|
||||
class ProviderConfigurationError(ProviderError):
|
||||
"""Exception raised by providers when badly configured"""
|
||||
class ConfigurationError(ProviderError):
|
||||
"""Exception raised by providers when badly configured."""
|
||||
pass
|
||||
|
||||
|
||||
class ProviderNotAvailable(ProviderError):
|
||||
"""Exception raised by providers when unavailable"""
|
||||
class AuthenticationError(ProviderError):
|
||||
"""Exception raised by providers when authentication failed."""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidSubtitle(ProviderError):
|
||||
"""Exception raised by providers when the downloaded subtitle is invalid"""
|
||||
class TooManyRequests(ProviderError):
|
||||
"""Exception raised by providers when too many requests are made."""
|
||||
pass
|
||||
|
||||
|
||||
class DownloadLimitExceeded(ProviderError):
|
||||
"""Exception raised by providers when download limit is exceeded."""
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from pkg_resources import EntryPoint
|
||||
|
||||
from stevedore import ExtensionManager
|
||||
|
||||
|
||||
class RegistrableExtensionManager(ExtensionManager):
|
||||
""":class:~stevedore.extensions.ExtensionManager` with support for registration.
|
||||
|
||||
It allows loading of internal extensions without setup and registering/unregistering additional extensions.
|
||||
|
||||
Loading is done in this order:
|
||||
|
||||
* Entry point extensions
|
||||
* Internal extensions
|
||||
* Registered extensions
|
||||
|
||||
:param str namespace: namespace argument for :class:~stevedore.extensions.ExtensionManager`.
|
||||
:param list internal_extensions: internal extensions to use with entry point syntax.
|
||||
:param \*\*kwargs: additional parameters for the :class:~stevedore.extensions.ExtensionManager` constructor.
|
||||
|
||||
"""
|
||||
def __init__(self, namespace, internal_extensions, **kwargs):
|
||||
#: Registered extensions with entry point syntax
|
||||
self.registered_extensions = []
|
||||
|
||||
#: Internal extensions with entry point syntax
|
||||
self.internal_extensions = internal_extensions
|
||||
|
||||
super(RegistrableExtensionManager, self).__init__(namespace, **kwargs)
|
||||
|
||||
def _find_entry_points(self, namespace):
|
||||
# copy of default extensions
|
||||
eps = list(super(RegistrableExtensionManager, self)._find_entry_points(namespace))
|
||||
|
||||
# internal extensions
|
||||
for iep in self.internal_extensions:
|
||||
ep = EntryPoint.parse(iep)
|
||||
if ep.name not in [e.name for e in eps]:
|
||||
eps.append(ep)
|
||||
|
||||
# registered extensions
|
||||
for rep in self.registered_extensions:
|
||||
ep = EntryPoint.parse(rep)
|
||||
if ep.name not in [e.name for e in eps]:
|
||||
eps.append(ep)
|
||||
|
||||
return eps
|
||||
|
||||
def register(self, entry_point):
|
||||
"""Register an extension
|
||||
|
||||
:param str entry_point: extension to register (entry point syntax).
|
||||
:raise: ValueError if already registered.
|
||||
|
||||
"""
|
||||
if entry_point in self.registered_extensions:
|
||||
raise ValueError('Extension already registered')
|
||||
|
||||
ep = EntryPoint.parse(entry_point)
|
||||
if ep.name in self.names():
|
||||
raise ValueError('An extension with the same name already exist')
|
||||
|
||||
ext = self._load_one_plugin(ep, False, (), {}, False)
|
||||
self.extensions.append(ext)
|
||||
if self._extensions_by_name is not None:
|
||||
self._extensions_by_name[ext.name] = ext
|
||||
self.registered_extensions.insert(0, entry_point)
|
||||
|
||||
def unregister(self, entry_point):
|
||||
"""Unregister a provider
|
||||
|
||||
:param str entry_point: provider to unregister (entry point syntax).
|
||||
|
||||
"""
|
||||
if entry_point not in self.registered_extensions:
|
||||
raise ValueError('Extension not registered')
|
||||
|
||||
ep = EntryPoint.parse(entry_point)
|
||||
self.registered_extensions.remove(entry_point)
|
||||
if self._extensions_by_name is not None:
|
||||
del self._extensions_by_name[ep.name]
|
||||
for i, ext in enumerate(self.extensions):
|
||||
if ext.name == ep.name:
|
||||
del self.extensions[i]
|
||||
break
|
||||
|
||||
|
||||
#: Provider manager
|
||||
provider_manager = RegistrableExtensionManager('subliminal.providers', [
|
||||
'addic7ed = subliminal.providers.addic7ed:Addic7edProvider',
|
||||
'legendastv = subliminal.providers.legendastv:LegendasTVProvider',
|
||||
'opensubtitles = subliminal.providers.opensubtitles:OpenSubtitlesProvider',
|
||||
'podnapisi = subliminal.providers.podnapisi:PodnapisiProvider',
|
||||
'shooter = subliminal.providers.shooter:ShooterProvider',
|
||||
'subscenter = subliminal.providers.subscenter:SubsCenterProvider',
|
||||
'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider',
|
||||
'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider'
|
||||
])
|
||||
|
||||
#: Refiner manager
|
||||
refiner_manager = RegistrableExtensionManager('subliminal.refiners', [
|
||||
'metadata = subliminal.refiners.metadata:refine',
|
||||
'omdb = subliminal.refiners.omdb:refine',
|
||||
'tvdb = subliminal.refiners.tvdb:refine'
|
||||
])
|
||||
@@ -1,19 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import babelfish
|
||||
import logging
|
||||
|
||||
from bs4 import BeautifulSoup, FeatureNotFound
|
||||
from six.moves.xmlrpc_client import SafeTransport
|
||||
|
||||
from ..video import Episode, Movie
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TimeoutSafeTransport(SafeTransport):
|
||||
"""Timeout support for ``xmlrpc.client.SafeTransport``."""
|
||||
def __init__(self, timeout, *args, **kwargs):
|
||||
SafeTransport.__init__(self, *args, **kwargs)
|
||||
self.timeout = timeout
|
||||
|
||||
def make_connection(self, host):
|
||||
c = SafeTransport.make_connection(self, host)
|
||||
c.timeout = self.timeout
|
||||
|
||||
return c
|
||||
|
||||
|
||||
class ParserBeautifulSoup(BeautifulSoup):
|
||||
"""A ``bs4.BeautifulSoup`` that picks the first parser available in `parsers`.
|
||||
|
||||
:param markup: markup for the ``bs4.BeautifulSoup``.
|
||||
:param list parsers: parser names, in order of preference.
|
||||
|
||||
"""
|
||||
def __init__(self, markup, parsers, **kwargs):
|
||||
# reject features
|
||||
if set(parsers).intersection({'fast', 'permissive', 'strict', 'xml', 'html', 'html5'}):
|
||||
raise ValueError('Features not allowed, only parser names')
|
||||
|
||||
# reject some kwargs
|
||||
if 'features' in kwargs:
|
||||
raise ValueError('Cannot use features kwarg')
|
||||
if 'builder' in kwargs:
|
||||
raise ValueError('Cannot use builder kwarg')
|
||||
|
||||
# pick the first parser available
|
||||
for parser in parsers:
|
||||
try:
|
||||
super(ParserBeautifulSoup, self).__init__(markup, parser, **kwargs)
|
||||
return
|
||||
except FeatureNotFound:
|
||||
pass
|
||||
|
||||
raise FeatureNotFound
|
||||
|
||||
|
||||
class Provider(object):
|
||||
"""Base class for providers
|
||||
"""Base class for providers.
|
||||
|
||||
If any configuration is possible for the provider, like credentials, it must take place during instantiation
|
||||
If any configuration is possible for the provider, like credentials, it must take place during instantiation.
|
||||
|
||||
:param \*\*kwargs: configuration
|
||||
:raise: :class:`~subliminal.exceptions.ProviderConfigurationError` if there is a configuration error
|
||||
:raise: :class:`~subliminal.exceptions.ConfigurationError` if there is a configuration error
|
||||
|
||||
"""
|
||||
#: Supported BabelFish languages
|
||||
#: Supported set of :class:`~babelfish.language.Language`
|
||||
languages = set()
|
||||
|
||||
#: Supported video types
|
||||
@@ -22,53 +68,46 @@ class Provider(object):
|
||||
#: Required hash, if any
|
||||
required_hash = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
self.initialize()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.terminate()
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize the provider
|
||||
"""Initialize the provider.
|
||||
|
||||
Must be called when starting to work with the provider. This is the place for network initialization
|
||||
or login operations.
|
||||
|
||||
.. note:
|
||||
This is called automatically if you use the :keyword:`with` statement
|
||||
|
||||
|
||||
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
|
||||
.. note::
|
||||
This is called automatically when entering the `with` statement
|
||||
|
||||
"""
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
def terminate(self):
|
||||
"""Terminate the provider
|
||||
"""Terminate the provider.
|
||||
|
||||
Must be called when done with the provider. This is the place for network shutdown or logout operations.
|
||||
|
||||
.. note:
|
||||
This is called automatically if you use the :keyword:`with` statement
|
||||
.. note::
|
||||
This is called automatically when exiting the `with` statement
|
||||
|
||||
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
|
||||
"""
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def check(cls, video):
|
||||
"""Check if the `video` can be processed
|
||||
"""Check if the `video` can be processed.
|
||||
|
||||
The video is considered invalid if not an instance of :attr:`video_types` or if the :attr:`required_hash` is
|
||||
not present in :attr:`~subliminal.video.Video`'s `hashes` attribute.
|
||||
The `video` is considered invalid if not an instance of :attr:`video_types` or if the :attr:`required_hash` is
|
||||
not present in :attr:`~subliminal.video.Video.hashes` attribute of the `video`.
|
||||
|
||||
:param video: the video to check
|
||||
:param video: the video to check.
|
||||
:type video: :class:`~subliminal.video.Video`
|
||||
:return: `True` if the `video` and `languages` are valid, `False` otherwise
|
||||
:return: `True` if the `video` is valid, `False` otherwise.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
@@ -76,53 +115,44 @@ class Provider(object):
|
||||
return False
|
||||
if cls.required_hash is not None and cls.required_hash not in video.hashes:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def query(self, languages, *args, **kwargs):
|
||||
"""Query the provider for subtitles
|
||||
def query(self, *args, **kwargs):
|
||||
"""Query the provider for subtitles.
|
||||
|
||||
This method arguments match as much as possible the actual parameters for querying the provider
|
||||
Arguments should match as much as possible the actual parameters for querying the provider
|
||||
|
||||
:param languages: languages to search for
|
||||
:type languages: set of :class:`babelfish.Language`
|
||||
:param \*args: other required arguments
|
||||
:param \*\*kwargs: other optional arguments
|
||||
:return: the subtitles
|
||||
:return: found subtitles.
|
||||
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
|
||||
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
|
||||
:raise: :class:`~subliminal.exceptions.ProviderError` if something unexpected occured
|
||||
:raise: :class:`~subliminal.exceptions.ProviderError`
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def list_subtitles(self, video, languages):
|
||||
"""List subtitles for the `video` with the given `languages`
|
||||
"""List subtitles for the `video` with the given `languages`.
|
||||
|
||||
This is a proxy for the :meth:`query` method. The parameters passed to the :meth:`query` method may
|
||||
vary depending on the amount of information available in the `video`
|
||||
This will call the :meth:`query` method internally. The parameters passed to the :meth:`query` method may
|
||||
vary depending on the amount of information available in the `video`.
|
||||
|
||||
:param video: video to list subtitles for
|
||||
:param video: video to list subtitles for.
|
||||
:type video: :class:`~subliminal.video.Video`
|
||||
:param languages: languages to search for
|
||||
:type languages: set of :class:`babelfish.Language`
|
||||
:return: the subtitles
|
||||
:param languages: languages to search for.
|
||||
:type languages: set of :class:`~babelfish.language.Language`
|
||||
:return: found subtitles.
|
||||
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
|
||||
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
|
||||
:raise: :class:`~subliminal.exceptions.ProviderError` if something unexpected occured
|
||||
:raise: :class:`~subliminal.exceptions.ProviderError`
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def download_subtitle(self, subtitle):
|
||||
"""Download the `subtitle`
|
||||
"""Download `subtitle`'s :attr:`~subliminal.subtitle.Subtitle.content`.
|
||||
|
||||
:param subtitle: subtitle to download
|
||||
:param subtitle: subtitle to download.
|
||||
:type subtitle: :class:`~subliminal.subtitle.Subtitle`
|
||||
:return: the subtitle text
|
||||
:rtype: string
|
||||
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable` if the provider is unavailable
|
||||
:raise: :class:`~subliminal.exceptions.InvalidSubtitle` if the downloaded subtitle is invalid
|
||||
:raise: :class:`~subliminal.exceptions.ProviderError` if something unexpected occured
|
||||
:raise: :class:`~subliminal.exceptions.ProviderError`
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
+211
-118
@@ -1,38 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import logging
|
||||
import babelfish
|
||||
import bs4
|
||||
import charade
|
||||
import requests
|
||||
from . import Provider
|
||||
from .. import __version__
|
||||
from ..cache import region
|
||||
from ..exceptions import ProviderConfigurationError, ProviderNotAvailable, InvalidSubtitle
|
||||
from ..subtitle import Subtitle, is_valid_subtitle
|
||||
from ..video import Episode
|
||||
import re
|
||||
|
||||
from babelfish import Language, language_converters
|
||||
from guessit import guessit
|
||||
from requests import Session
|
||||
|
||||
from . import ParserBeautifulSoup, Provider
|
||||
from .. import __short_version__
|
||||
from ..cache import SHOW_EXPIRATION_TIME, region
|
||||
from ..exceptions import AuthenticationError, ConfigurationError, DownloadLimitExceeded, TooManyRequests
|
||||
from ..score import get_equivalent_release_groups
|
||||
from ..subtitle import Subtitle, fix_line_ending, guess_matches
|
||||
from ..utils import sanitize, sanitize_release_group
|
||||
from ..video import Episode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
language_converters.register('addic7ed = subliminal.converters.addic7ed:Addic7edConverter')
|
||||
|
||||
#: Series header parsing regex
|
||||
series_year_re = re.compile(r'^(?P<series>[ \w\'.:(),&!?-]+?)(?: \((?P<year>\d{4})\))?$')
|
||||
|
||||
|
||||
class Addic7edSubtitle(Subtitle):
|
||||
"""Addic7ed Subtitle."""
|
||||
provider_name = 'addic7ed'
|
||||
|
||||
def __init__(self, language, series, season, episode, title, version, hearing_impaired, download_link, referer):
|
||||
super(Addic7edSubtitle, self).__init__(language, hearing_impaired)
|
||||
def __init__(self, language, hearing_impaired, page_link, series, season, episode, title, year, version,
|
||||
download_link):
|
||||
super(Addic7edSubtitle, self).__init__(language, hearing_impaired, page_link)
|
||||
self.series = series
|
||||
self.season = season
|
||||
self.episode = episode
|
||||
self.title = title
|
||||
self.year = year
|
||||
self.version = version
|
||||
self.download_link = download_link
|
||||
self.referer = referer
|
||||
|
||||
def compute_matches(self, video):
|
||||
@property
|
||||
def id(self):
|
||||
return self.download_link
|
||||
|
||||
def get_matches(self, video):
|
||||
matches = set()
|
||||
|
||||
# series
|
||||
if video.series and self.series == video.series:
|
||||
if video.series and sanitize(self.series) == sanitize(video.series):
|
||||
matches.add('series')
|
||||
# season
|
||||
if video.season and self.season == video.season:
|
||||
@@ -41,154 +55,233 @@ class Addic7edSubtitle(Subtitle):
|
||||
if video.episode and self.episode == video.episode:
|
||||
matches.add('episode')
|
||||
# title
|
||||
if video.title and self.title.lower() == video.title.lower():
|
||||
if video.title and sanitize(self.title) == sanitize(video.title):
|
||||
matches.add('title')
|
||||
# year
|
||||
if video.original_series and self.year is None or video.year and video.year == self.year:
|
||||
matches.add('year')
|
||||
# release_group
|
||||
if video.release_group and self.version and video.release_group.lower() in self.version.lower():
|
||||
if (video.release_group and self.version and
|
||||
any(r in sanitize_release_group(self.version)
|
||||
for r in get_equivalent_release_groups(sanitize_release_group(video.release_group)))):
|
||||
matches.add('release_group')
|
||||
# resolution
|
||||
if video.resolution and self.version and video.resolution in self.version.lower():
|
||||
matches.add('resolution')
|
||||
# format
|
||||
if video.format and self.version and video.format.lower() in self.version.lower():
|
||||
matches.add('format')
|
||||
# other properties
|
||||
matches |= guess_matches(video, guessit(self.version), partial=True)
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
class Addic7edProvider(Provider):
|
||||
languages = {babelfish.Language('por', 'BR')} | {babelfish.Language(l)
|
||||
for l in ['ara', 'aze', 'ben', 'bos', 'bul', 'cat', 'ces', 'dan', 'deu', 'ell', 'eng', 'eus', 'fas',
|
||||
'fin', 'fra', 'glg', 'heb', 'hrv', 'hun', 'hye', 'ind', 'ita', 'jpn', 'kor', 'mkd', 'msa',
|
||||
'nld', 'nor', 'pol', 'por', 'ron', 'rus', 'slk', 'slv', 'spa', 'sqi', 'srp', 'swe', 'tha',
|
||||
'tur', 'ukr', 'vie', 'zho']}
|
||||
"""Addic7ed Provider."""
|
||||
languages = {Language('por', 'BR')} | {Language(l) for l in [
|
||||
'ara', 'aze', 'ben', 'bos', 'bul', 'cat', 'ces', 'dan', 'deu', 'ell', 'eng', 'eus', 'fas', 'fin', 'fra', 'glg',
|
||||
'heb', 'hrv', 'hun', 'hye', 'ind', 'ita', 'jpn', 'kor', 'mkd', 'msa', 'nld', 'nor', 'pol', 'por', 'ron', 'rus',
|
||||
'slk', 'slv', 'spa', 'sqi', 'srp', 'swe', 'tha', 'tur', 'ukr', 'vie', 'zho'
|
||||
]}
|
||||
video_types = (Episode,)
|
||||
server = 'http://www.addic7ed.com'
|
||||
server_url = 'http://www.addic7ed.com/'
|
||||
|
||||
def __init__(self, username=None, password=None):
|
||||
if username is not None and password is None or username is None and password is not None:
|
||||
raise ProviderConfigurationError('Username and password must be specified')
|
||||
raise ConfigurationError('Username and password must be specified')
|
||||
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.logged_in = False
|
||||
|
||||
def initialize(self):
|
||||
self.session = requests.Session()
|
||||
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
|
||||
self.session = Session()
|
||||
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
|
||||
|
||||
# login
|
||||
if self.username is not None and self.password is not None:
|
||||
logger.debug('Logging in')
|
||||
logger.info('Logging in')
|
||||
data = {'username': self.username, 'password': self.password, 'Submit': 'Log in'}
|
||||
try:
|
||||
r = self.session.post(self.server + '/dologin.php', data, timeout=10, allow_redirects=False)
|
||||
except requests.Timeout:
|
||||
raise ProviderNotAvailable('Timeout after 10 seconds')
|
||||
if r.status_code == 302:
|
||||
logger.info('Logged in')
|
||||
self.logged_in = True
|
||||
else:
|
||||
logger.error('Failed to login')
|
||||
r = self.session.post(self.server_url + 'dologin.php', data, allow_redirects=False, timeout=10)
|
||||
|
||||
if r.status_code != 302:
|
||||
raise AuthenticationError(self.username)
|
||||
|
||||
logger.debug('Logged in')
|
||||
self.logged_in = True
|
||||
|
||||
def terminate(self):
|
||||
# logout
|
||||
if self.logged_in:
|
||||
try:
|
||||
r = self.session.get(self.server + '/logout.php', timeout=10)
|
||||
logger.info('Logged out')
|
||||
except requests.Timeout:
|
||||
raise ProviderNotAvailable('Timeout after 10 seconds')
|
||||
if r.status_code != 200:
|
||||
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
|
||||
logger.info('Logging out')
|
||||
r = self.session.get(self.server_url + 'logout.php', timeout=10)
|
||||
r.raise_for_status()
|
||||
logger.debug('Logged out')
|
||||
self.logged_in = False
|
||||
|
||||
self.session.close()
|
||||
|
||||
def get(self, url, params=None):
|
||||
"""Make a GET request on `url` with the given parameters
|
||||
@region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME)
|
||||
def _get_show_ids(self):
|
||||
"""Get the ``dict`` of show ids per series by querying the `shows.php` page.
|
||||
|
||||
:param string url: part of the URL to reach with the leading slash
|
||||
:param params: params of the request
|
||||
:return: the response
|
||||
:rtype: :class:`bs4.BeautifulSoup`
|
||||
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
|
||||
|
||||
"""
|
||||
try:
|
||||
r = self.session.get(self.server + url, params=params, timeout=10)
|
||||
except requests.Timeout:
|
||||
raise ProviderNotAvailable('Timeout after 10 seconds')
|
||||
if r.status_code != 200:
|
||||
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
|
||||
return bs4.BeautifulSoup(r.content, ['permissive'])
|
||||
|
||||
@region.cache_on_arguments()
|
||||
def get_show_ids(self):
|
||||
"""Load the shows page with default series to show ids mapping
|
||||
|
||||
:return: series to show ids
|
||||
:return: show id per series, lower case and without quotes.
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
soup = self.get('/shows.php')
|
||||
# get the show page
|
||||
logger.info('Getting show ids')
|
||||
r = self.session.get(self.server_url + 'shows.php', timeout=10)
|
||||
r.raise_for_status()
|
||||
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
|
||||
|
||||
# populate the show ids
|
||||
show_ids = {}
|
||||
for html_show in soup.select('td.version > h3 > a[href^="/show/"]'):
|
||||
show_ids[html_show.string.lower()] = int(html_show['href'][6:])
|
||||
for show in soup.select('td.version > h3 > a[href^="/show/"]'):
|
||||
show_ids[sanitize(show.text)] = int(show['href'][6:])
|
||||
logger.debug('Found %d show ids', len(show_ids))
|
||||
|
||||
return show_ids
|
||||
|
||||
@region.cache_on_arguments()
|
||||
def find_show_id(self, series):
|
||||
"""Find a show id from the series
|
||||
@region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME)
|
||||
def _search_show_id(self, series, year=None):
|
||||
"""Search the show id from the `series` and `year`.
|
||||
|
||||
Use this only if the series is not in the dict returned by :meth:`get_show_ids`
|
||||
|
||||
:param string series: series of the episode
|
||||
:return: the show id, if any
|
||||
:rtype: int or None
|
||||
:param str series: series of the episode.
|
||||
:param year: year of the series, if any.
|
||||
:type year: int
|
||||
:return: the show id, if found.
|
||||
:rtype: int
|
||||
|
||||
"""
|
||||
params = {'search': series, 'Submit': 'Search'}
|
||||
logger.debug('Searching series %r', params)
|
||||
suggested_shows = self.get('/search.php', params).select('span.titulo > a[href^="/show/"]')
|
||||
if not suggested_shows:
|
||||
logger.info('Series %r not found', series)
|
||||
return None
|
||||
return int(suggested_shows[0]['href'][6:])
|
||||
# addic7ed doesn't support search with quotes
|
||||
series = series.replace('\'', ' ')
|
||||
|
||||
def query(self, series, season):
|
||||
show_ids = self.get_show_ids()
|
||||
if series.lower() in show_ids:
|
||||
show_id = show_ids[series.lower()]
|
||||
else:
|
||||
show_id = self.find_show_id(series.lower())
|
||||
if show_id is None:
|
||||
return []
|
||||
params = {'show_id': show_id, 'season': season}
|
||||
logger.debug('Searching subtitles %r', params)
|
||||
link = '/show/{show_id}&season={season}'.format(**params)
|
||||
soup = self.get(link)
|
||||
# build the params
|
||||
series_year = '%s %d' % (series, year) if year is not None else series
|
||||
params = {'search': series_year, 'Submit': 'Search'}
|
||||
|
||||
# make the search
|
||||
logger.info('Searching show ids with %r', params)
|
||||
r = self.session.get(self.server_url + 'search.php', params=params, timeout=10)
|
||||
r.raise_for_status()
|
||||
if r.status_code == 304:
|
||||
raise TooManyRequests()
|
||||
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
|
||||
|
||||
# get the suggestion
|
||||
suggestion = soup.select('span.titulo > a[href^="/show/"]')
|
||||
if not suggestion:
|
||||
logger.warning('Show id not found: no suggestion')
|
||||
return None
|
||||
if not sanitize(suggestion[0].i.text.replace('\'', ' ')) == sanitize(series_year):
|
||||
logger.warning('Show id not found: suggestion does not match')
|
||||
return None
|
||||
show_id = int(suggestion[0]['href'][6:])
|
||||
logger.debug('Found show id %d', show_id)
|
||||
|
||||
return show_id
|
||||
|
||||
def get_show_id(self, series, year=None, country_code=None):
|
||||
"""Get the best matching show id for `series`, `year` and `country_code`.
|
||||
|
||||
First search in the result of :meth:`_get_show_ids` and fallback on a search with :meth:`_search_show_id`.
|
||||
|
||||
:param str series: series of the episode.
|
||||
:param year: year of the series, if any.
|
||||
:type year: int
|
||||
:param country_code: country code of the series, if any.
|
||||
:type country_code: str
|
||||
:return: the show id, if found.
|
||||
:rtype: int
|
||||
|
||||
"""
|
||||
series_sanitized = sanitize(series).lower()
|
||||
show_ids = self._get_show_ids()
|
||||
show_id = None
|
||||
|
||||
# attempt with country
|
||||
if not show_id and country_code:
|
||||
logger.debug('Getting show id with country')
|
||||
show_id = show_ids.get('%s %s' % (series_sanitized, country_code.lower()))
|
||||
|
||||
# attempt with year
|
||||
if not show_id and year:
|
||||
logger.debug('Getting show id with year')
|
||||
show_id = show_ids.get('%s %d' % (series_sanitized, year))
|
||||
|
||||
# attempt clean
|
||||
if not show_id:
|
||||
logger.debug('Getting show id')
|
||||
show_id = show_ids.get(series_sanitized)
|
||||
|
||||
# search as last resort
|
||||
if not show_id:
|
||||
logger.warning('Series not found in show ids')
|
||||
show_id = self._search_show_id(series)
|
||||
|
||||
return show_id
|
||||
|
||||
def query(self, series, season, year=None, country=None):
|
||||
# get the show id
|
||||
show_id = self.get_show_id(series, year, country)
|
||||
if show_id is None:
|
||||
logger.error('No show id found for %r (%r)', series, {'year': year, 'country': country})
|
||||
return []
|
||||
|
||||
# get the page of the season of the show
|
||||
logger.info('Getting the page of show id %d, season %d', show_id, season)
|
||||
r = self.session.get(self.server_url + 'show/%d' % show_id, params={'season': season}, timeout=10)
|
||||
r.raise_for_status()
|
||||
if r.status_code == 304:
|
||||
raise TooManyRequests()
|
||||
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
|
||||
|
||||
# loop over subtitle rows
|
||||
match = series_year_re.match(soup.select('#header font')[0].text.strip()[:-10])
|
||||
series = match.group('series')
|
||||
year = int(match.group('year')) if match.group('year') else None
|
||||
subtitles = []
|
||||
for row in soup('tr', class_='epeven completed'):
|
||||
for row in soup.select('tr.epeven'):
|
||||
cells = row('td')
|
||||
if cells[5].string != 'Completed':
|
||||
logger.debug('Skipping incomplete subtitle')
|
||||
|
||||
# ignore incomplete subtitles
|
||||
status = cells[5].text
|
||||
if status != 'Completed':
|
||||
logger.debug('Ignoring subtitle with status %s', status)
|
||||
continue
|
||||
if not cells[3].string:
|
||||
logger.debug('Skipping empty language')
|
||||
continue
|
||||
subtitles.append(Addic7edSubtitle(babelfish.Language.fromaddic7ed(cells[3].string), series, season,
|
||||
int(cells[1].string), cells[2].string, cells[4].string,
|
||||
bool(cells[6].string), cells[9].a['href'], link))
|
||||
|
||||
# read the item
|
||||
language = Language.fromaddic7ed(cells[3].text)
|
||||
hearing_impaired = bool(cells[6].text)
|
||||
page_link = self.server_url + cells[2].a['href'][1:]
|
||||
season = int(cells[0].text)
|
||||
episode = int(cells[1].text)
|
||||
title = cells[2].text
|
||||
version = cells[4].text
|
||||
download_link = cells[9].a['href'][1:]
|
||||
|
||||
subtitle = Addic7edSubtitle(language, hearing_impaired, page_link, series, season, episode, title, year,
|
||||
version, download_link)
|
||||
logger.debug('Found subtitle %r', subtitle)
|
||||
subtitles.append(subtitle)
|
||||
|
||||
return subtitles
|
||||
|
||||
def list_subtitles(self, video, languages):
|
||||
return [s for s in self.query(video.series, video.season)
|
||||
return [s for s in self.query(video.series, video.season, video.year)
|
||||
if s.language in languages and s.episode == video.episode]
|
||||
|
||||
def download_subtitle(self, subtitle):
|
||||
try:
|
||||
r = self.session.get(self.server + subtitle.download_link, timeout=10,
|
||||
headers={'Referer': self.server + subtitle.referer})
|
||||
except requests.Timeout:
|
||||
raise ProviderNotAvailable('Timeout after 10 seconds')
|
||||
if r.status_code != 200:
|
||||
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
|
||||
# download the subtitle
|
||||
logger.info('Downloading subtitle %r', subtitle)
|
||||
r = self.session.get(self.server_url + subtitle.download_link, headers={'Referer': subtitle.page_link},
|
||||
timeout=10)
|
||||
r.raise_for_status()
|
||||
|
||||
# detect download limit exceeded
|
||||
if r.headers['Content-Type'] == 'text/html':
|
||||
raise ProviderNotAvailable('Download limit exceeded')
|
||||
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
|
||||
if not is_valid_subtitle(subtitle_text):
|
||||
raise InvalidSubtitle
|
||||
return subtitle_text
|
||||
raise DownloadLimitExceeded
|
||||
|
||||
subtitle.content = fix_line_ending(r.content)
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import logging
|
||||
import urllib
|
||||
import babelfish
|
||||
import charade
|
||||
import guessit
|
||||
import requests
|
||||
import xml.etree.ElementTree
|
||||
from . import Provider
|
||||
from .. import __version__
|
||||
from ..cache import region
|
||||
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
|
||||
from ..subtitle import Subtitle, is_valid_subtitle, compute_guess_matches
|
||||
from ..video import Episode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BierDopjeSubtitle(Subtitle):
|
||||
provider_name = 'bierdopje'
|
||||
|
||||
def __init__(self, language, season, episode, tvdb_id, series, filename, download_link):
|
||||
super(BierDopjeSubtitle, self).__init__(language)
|
||||
self.season = season
|
||||
self.episode = episode
|
||||
self.tvdb_id = tvdb_id
|
||||
self.series = series
|
||||
self.filename = filename
|
||||
self.download_link = download_link
|
||||
|
||||
def compute_matches(self, video):
|
||||
matches = set()
|
||||
# tvdb_id
|
||||
if video.tvdb_id and self.tvdb_id == video.tvdb_id:
|
||||
matches.add('tvdb_id')
|
||||
# series
|
||||
if video.series and self.series == video.series:
|
||||
matches.add('series')
|
||||
# season
|
||||
if video.season and self.season == video.season:
|
||||
matches.add('season')
|
||||
# episode
|
||||
if video.episode and self.episode == video.episode:
|
||||
matches.add('episode')
|
||||
matches |= compute_guess_matches(video, guessit.guess_episode_info(self.filename + '.mkv'))
|
||||
return matches
|
||||
|
||||
|
||||
class BierDopjeProvider(Provider):
|
||||
languages = {babelfish.Language(l) for l in ['eng', 'nld']}
|
||||
video_types = (Episode,)
|
||||
|
||||
def initialize(self):
|
||||
self.session = requests.Session()
|
||||
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
|
||||
|
||||
def terminate(self):
|
||||
self.session.close()
|
||||
|
||||
def get(self, url, **params):
|
||||
"""Make a GET request on the `url` formatted with `**params`
|
||||
|
||||
:param string url: API part of the URL to reach without the leading slash
|
||||
:param \*\*params: format specs for the `url`
|
||||
:return: the response
|
||||
:rtype: :class:`xml.etree.ElementTree.Element`
|
||||
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
|
||||
|
||||
"""
|
||||
try:
|
||||
r = self.session.get('http://api.bierdopje.com/A2B638AC5D804C2E/' + url.format(**params), timeout=10)
|
||||
except requests.Timeout:
|
||||
raise ProviderNotAvailable('Timeout after 10 seconds')
|
||||
if r.status_code == 429:
|
||||
raise ProviderNotAvailable('Too Many Requests')
|
||||
elif r.status_code != 200:
|
||||
raise ProviderError('Request failed with status code %d' % r.status_code)
|
||||
return xml.etree.ElementTree.fromstring(r.content)
|
||||
|
||||
@region.cache_on_arguments()
|
||||
def find_show_id(self, series):
|
||||
"""Find the show id from series name
|
||||
|
||||
:param string series: series of the episode
|
||||
:return: show id
|
||||
:rtype: int
|
||||
|
||||
"""
|
||||
logger.debug('Searching for series %r', series)
|
||||
root = self.get('FindShowByName/{series}', series=urllib.quote(series))
|
||||
if root.find('response/status').text == 'false':
|
||||
logger.info('Series %r not found', series)
|
||||
return None
|
||||
return int(root.find('response/results/result[1]/showid').text)
|
||||
|
||||
def query(self, language, season, episode, tvdb_id=None, series=None):
|
||||
params = {'language': language.alpha2, 'season': season, 'episode': episode}
|
||||
if tvdb_id is not None:
|
||||
params['showid'] = tvdb_id
|
||||
params['istvdbid'] = 'true'
|
||||
elif series is not None:
|
||||
show_id = self.find_show_id(series)
|
||||
if show_id is None:
|
||||
return []
|
||||
params['showid'] = show_id
|
||||
params['istvdbid'] = 'false'
|
||||
else:
|
||||
raise ValueError('Missing parameter tvdb_id or series')
|
||||
logger.debug('Searching subtitles %r', params)
|
||||
root = self.get('GetAllSubsFor/{showid}/{season}/{episode}/{language}/{istvdbid}', **params)
|
||||
if root.find('response/status').text == 'false':
|
||||
logger.debug('No subtitle found')
|
||||
return []
|
||||
logger.debug('Found subtitles %r', root.find('response/results'))
|
||||
return [BierDopjeSubtitle(language, season, episode, tvdb_id, series, result.find('filename').text,
|
||||
result.find('downloadlink').text) for result in root.find('response/results')]
|
||||
|
||||
def list_subtitles(self, video, languages):
|
||||
return [s for l in languages for s in self.query(l, video.season, video.episode, video.tvdb_id, video.series)]
|
||||
|
||||
def download_subtitle(self, subtitle):
|
||||
try:
|
||||
r = self.session.get(subtitle.download_link, timeout=10)
|
||||
except requests.Timeout:
|
||||
raise ProviderNotAvailable('Timeout after 10 seconds')
|
||||
if r.status_code == 429:
|
||||
raise ProviderNotAvailable('Too Many Requests')
|
||||
elif r.status_code != 200:
|
||||
raise ProviderError('Request failed with status code %d' % r.status_code)
|
||||
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
|
||||
if not is_valid_subtitle(subtitle_text):
|
||||
raise InvalidSubtitle
|
||||
return subtitle_text
|
||||
@@ -0,0 +1,448 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from babelfish import Language, language_converters
|
||||
from datetime import datetime, timedelta
|
||||
from dogpile.cache.api import NO_VALUE
|
||||
from guessit import guessit
|
||||
import pytz
|
||||
import rarfile
|
||||
from rarfile import RarFile, is_rarfile
|
||||
from requests import Session
|
||||
from zipfile import ZipFile, is_zipfile
|
||||
|
||||
from . import ParserBeautifulSoup, Provider
|
||||
from .. import __short_version__
|
||||
from ..cache import SHOW_EXPIRATION_TIME, region
|
||||
from ..exceptions import AuthenticationError, ConfigurationError, ProviderError
|
||||
from ..subtitle import SUBTITLE_EXTENSIONS, Subtitle, fix_line_ending, guess_matches, sanitize
|
||||
from ..video import Episode, Movie
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
language_converters.register('legendastv = subliminal.converters.legendastv:LegendasTVConverter')
|
||||
|
||||
# Configure :mod:`rarfile` to use the same path separator as :mod:`zipfile`
|
||||
rarfile.PATH_SEP = '/'
|
||||
|
||||
#: Conversion map for types
|
||||
type_map = {'M': 'movie', 'S': 'episode', 'C': 'episode'}
|
||||
|
||||
#: BR title season parsing regex
|
||||
season_re = re.compile(r' - (?P<season>\d+)(\xaa|a|st|nd|rd|th) (temporada|season)', re.IGNORECASE)
|
||||
|
||||
#: Downloads parsing regex
|
||||
downloads_re = re.compile(r'(?P<downloads>\d+) downloads')
|
||||
|
||||
#: Rating parsing regex
|
||||
rating_re = re.compile(r'nota (?P<rating>\d+)')
|
||||
|
||||
#: Timestamp parsing regex
|
||||
timestamp_re = re.compile(r'(?P<day>\d+)/(?P<month>\d+)/(?P<year>\d+) - (?P<hour>\d+):(?P<minute>\d+)')
|
||||
|
||||
#: Cache key for releases
|
||||
releases_key = __name__ + ':releases|{archive_id}'
|
||||
|
||||
|
||||
class LegendasTVArchive(object):
|
||||
"""LegendasTV Archive.
|
||||
|
||||
:param str id: identifier.
|
||||
:param str name: name.
|
||||
:param bool pack: contains subtitles for multiple episodes.
|
||||
:param bool pack: featured.
|
||||
:param str link: link.
|
||||
:param int downloads: download count.
|
||||
:param int rating: rating (0-10).
|
||||
:param timestamp: timestamp.
|
||||
:type timestamp: datetime.datetime
|
||||
|
||||
"""
|
||||
def __init__(self, id, name, pack, featured, link, downloads=0, rating=0, timestamp=None):
|
||||
#: Identifier
|
||||
self.id = id
|
||||
|
||||
#: Name
|
||||
self.name = name
|
||||
|
||||
#: Pack
|
||||
self.pack = pack
|
||||
|
||||
#: Featured
|
||||
self.featured = featured
|
||||
|
||||
#: Link
|
||||
self.link = link
|
||||
|
||||
#: Download count
|
||||
self.downloads = downloads
|
||||
|
||||
#: Rating (0-10)
|
||||
self.rating = rating
|
||||
|
||||
#: Timestamp
|
||||
self.timestamp = timestamp
|
||||
|
||||
#: Compressed content as :class:`rarfile.RarFile` or :class:`zipfile.ZipFile`
|
||||
self.content = None
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s [%s] %r>' % (self.__class__.__name__, self.id, self.name)
|
||||
|
||||
|
||||
class LegendasTVSubtitle(Subtitle):
|
||||
"""LegendasTV Subtitle."""
|
||||
provider_name = 'legendastv'
|
||||
|
||||
def __init__(self, language, type, title, year, imdb_id, season, archive, name):
|
||||
super(LegendasTVSubtitle, self).__init__(language, archive.link)
|
||||
self.type = type
|
||||
self.title = title
|
||||
self.year = year
|
||||
self.imdb_id = imdb_id
|
||||
self.season = season
|
||||
self.archive = archive
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return '%s-%s' % (self.archive.id, self.name.lower())
|
||||
|
||||
def get_matches(self, video, hearing_impaired=False):
|
||||
matches = set()
|
||||
|
||||
# episode
|
||||
if isinstance(video, Episode) and self.type == 'episode':
|
||||
# series
|
||||
if video.series and sanitize(self.title) == sanitize(video.series):
|
||||
matches.add('series')
|
||||
|
||||
# year (year is based on season air date hence the adjustment)
|
||||
if video.original_series and self.year is None or video.year and video.year == self.year - self.season + 1:
|
||||
matches.add('year')
|
||||
|
||||
# imdb_id
|
||||
if video.series_imdb_id and self.imdb_id == video.series_imdb_id:
|
||||
matches.add('series_imdb_id')
|
||||
|
||||
# movie
|
||||
elif isinstance(video, Movie) and self.type == 'movie':
|
||||
# title
|
||||
if video.title and sanitize(self.title) == sanitize(video.title):
|
||||
matches.add('title')
|
||||
|
||||
# year
|
||||
if video.year and self.year == video.year:
|
||||
matches.add('year')
|
||||
|
||||
# imdb_id
|
||||
if video.imdb_id and self.imdb_id == video.imdb_id:
|
||||
matches.add('imdb_id')
|
||||
|
||||
# archive name
|
||||
matches |= guess_matches(video, guessit(self.archive.name, {'type': self.type}))
|
||||
|
||||
# name
|
||||
matches |= guess_matches(video, guessit(self.name, {'type': self.type}))
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
class LegendasTVProvider(Provider):
|
||||
"""LegendasTV Provider.
|
||||
|
||||
:param str username: username.
|
||||
:param str password: password.
|
||||
|
||||
"""
|
||||
languages = {Language.fromlegendastv(l) for l in language_converters['legendastv'].codes}
|
||||
server_url = 'http://legendas.tv/'
|
||||
|
||||
def __init__(self, username=None, password=None):
|
||||
if username and not password or not username and password:
|
||||
raise ConfigurationError('Username and password must be specified')
|
||||
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.logged_in = False
|
||||
|
||||
def initialize(self):
|
||||
self.session = Session()
|
||||
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
|
||||
|
||||
# login
|
||||
if self.username is not None and self.password is not None:
|
||||
logger.info('Logging in')
|
||||
data = {'_method': 'POST', 'data[User][username]': self.username, 'data[User][password]': self.password}
|
||||
r = self.session.post(self.server_url + 'login', data, allow_redirects=False, timeout=10)
|
||||
r.raise_for_status()
|
||||
|
||||
soup = ParserBeautifulSoup(r.content, ['html.parser'])
|
||||
if soup.find('div', {'class': 'alert-error'}, string=re.compile(u'Usuário ou senha inválidos')):
|
||||
raise AuthenticationError(self.username)
|
||||
|
||||
logger.debug('Logged in')
|
||||
self.logged_in = True
|
||||
|
||||
def terminate(self):
|
||||
# logout
|
||||
if self.logged_in:
|
||||
logger.info('Logging out')
|
||||
r = self.session.get(self.server_url + 'users/logout', allow_redirects=False, timeout=10)
|
||||
r.raise_for_status()
|
||||
logger.debug('Logged out')
|
||||
self.logged_in = False
|
||||
|
||||
self.session.close()
|
||||
|
||||
@region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME)
|
||||
def search_titles(self, title):
|
||||
"""Search for titles matching the `title`.
|
||||
|
||||
:param str title: the title to search for.
|
||||
:return: found titles.
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
# make the query
|
||||
logger.info('Searching title %r', title)
|
||||
r = self.session.get(self.server_url + 'legenda/sugestao/{}'.format(title), timeout=10)
|
||||
r.raise_for_status()
|
||||
results = json.loads(r.text)
|
||||
|
||||
# loop over results
|
||||
titles = {}
|
||||
for result in results:
|
||||
source = result['_source']
|
||||
|
||||
# extract id
|
||||
title_id = int(source['id_filme'])
|
||||
|
||||
# extract type and title
|
||||
title = {'type': type_map[source['tipo']], 'title': source['dsc_nome']}
|
||||
|
||||
# extract year
|
||||
if source['dsc_data_lancamento'] and source['dsc_data_lancamento'].isdigit():
|
||||
title['year'] = int(source['dsc_data_lancamento'])
|
||||
|
||||
# extract imdb_id
|
||||
if source['id_imdb'] != '0':
|
||||
if not source['id_imdb'].startswith('tt'):
|
||||
title['imdb_id'] = 'tt' + source['id_imdb'].zfill(7)
|
||||
else:
|
||||
title['imdb_id'] = source['id_imdb']
|
||||
|
||||
# extract season
|
||||
if title['type'] == 'episode':
|
||||
if source['temporada'] and source['temporada'].isdigit():
|
||||
title['season'] = int(source['temporada'])
|
||||
else:
|
||||
match = season_re.search(source['dsc_nome_br'])
|
||||
if match:
|
||||
title['season'] = int(match.group('season'))
|
||||
else:
|
||||
logger.warning('No season detected for title %d', title_id)
|
||||
|
||||
# add title
|
||||
titles[title_id] = title
|
||||
|
||||
logger.debug('Found %d titles', len(titles))
|
||||
|
||||
return titles
|
||||
|
||||
@region.cache_on_arguments(expiration_time=timedelta(minutes=15).total_seconds())
|
||||
def get_archives(self, title_id, language_code):
|
||||
"""Get the archive list from a given `title_id` and `language_code`.
|
||||
|
||||
:param int title_id: title id.
|
||||
:param int language_code: language code.
|
||||
:return: the archives.
|
||||
:rtype: list of :class:`LegendasTVArchive`
|
||||
|
||||
"""
|
||||
logger.info('Getting archives for title %d and language %d', title_id, language_code)
|
||||
archives = []
|
||||
page = 1
|
||||
while True:
|
||||
# get the archive page
|
||||
url = self.server_url + 'util/carrega_legendas_busca_filme/{title}/{language}/-/{page}'.format(
|
||||
title=title_id, language=language_code, page=page)
|
||||
r = self.session.get(url)
|
||||
r.raise_for_status()
|
||||
|
||||
# parse the results
|
||||
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
|
||||
for archive_soup in soup.select('div.list_element > article > div'):
|
||||
# create archive
|
||||
archive = LegendasTVArchive(archive_soup.a['href'].split('/')[2], archive_soup.a.text,
|
||||
'pack' in archive_soup['class'], 'destaque' in archive_soup['class'],
|
||||
self.server_url + archive_soup.a['href'][1:])
|
||||
|
||||
# extract text containing downloads, rating and timestamp
|
||||
data_text = archive_soup.find('p', class_='data').text
|
||||
|
||||
# match downloads
|
||||
archive.downloads = int(downloads_re.search(data_text).group('downloads'))
|
||||
|
||||
# match rating
|
||||
match = rating_re.search(data_text)
|
||||
if match:
|
||||
archive.rating = int(match.group('rating'))
|
||||
|
||||
# match timestamp and validate it
|
||||
time_data = {k: int(v) for k, v in timestamp_re.search(data_text).groupdict().items()}
|
||||
archive.timestamp = pytz.timezone('America/Sao_Paulo').localize(datetime(**time_data))
|
||||
if archive.timestamp > datetime.utcnow().replace(tzinfo=pytz.utc):
|
||||
raise ProviderError('Archive timestamp is in the future')
|
||||
|
||||
# add archive
|
||||
archives.append(archive)
|
||||
|
||||
# stop on last page
|
||||
if soup.find('a', attrs={'class': 'load_more'}, string='carregar mais') is None:
|
||||
break
|
||||
|
||||
# increment page count
|
||||
page += 1
|
||||
|
||||
logger.debug('Found %d archives', len(archives))
|
||||
|
||||
return archives
|
||||
|
||||
def download_archive(self, archive):
|
||||
"""Download an archive's :attr:`~LegendasTVArchive.content`.
|
||||
|
||||
:param archive: the archive to download :attr:`~LegendasTVArchive.content` of.
|
||||
:type archive: :class:`LegendasTVArchive`
|
||||
|
||||
"""
|
||||
logger.info('Downloading archive %s', archive.id)
|
||||
r = self.session.get(self.server_url + 'downloadarquivo/{}'.format(archive.id))
|
||||
r.raise_for_status()
|
||||
|
||||
# open the archive
|
||||
archive_stream = io.BytesIO(r.content)
|
||||
if is_rarfile(archive_stream):
|
||||
logger.debug('Identified rar archive')
|
||||
archive.content = RarFile(archive_stream)
|
||||
elif is_zipfile(archive_stream):
|
||||
logger.debug('Identified zip archive')
|
||||
archive.content = ZipFile(archive_stream)
|
||||
else:
|
||||
raise ValueError('Not a valid archive')
|
||||
|
||||
def query(self, language, title, season=None, episode=None, year=None):
|
||||
# search for titles
|
||||
titles = self.search_titles(sanitize(title))
|
||||
|
||||
# search for titles with the quote or dot character
|
||||
ignore_characters = {'\'', '.'}
|
||||
if any(c in title for c in ignore_characters):
|
||||
titles.update(self.search_titles(sanitize(title, ignore_characters=ignore_characters)))
|
||||
|
||||
subtitles = []
|
||||
# iterate over titles
|
||||
for title_id, t in titles.items():
|
||||
# discard mismatches on title
|
||||
if sanitize(t['title']) != sanitize(title):
|
||||
continue
|
||||
|
||||
# episode
|
||||
if season and episode:
|
||||
# discard mismatches on type
|
||||
if t['type'] != 'episode':
|
||||
continue
|
||||
|
||||
# discard mismatches on season
|
||||
if 'season' not in t or t['season'] != season:
|
||||
continue
|
||||
# movie
|
||||
else:
|
||||
# discard mismatches on type
|
||||
if t['type'] != 'movie':
|
||||
continue
|
||||
|
||||
# discard mismatches on year
|
||||
if year is not None and 'year' in t and t['year'] != year:
|
||||
continue
|
||||
|
||||
# iterate over title's archives
|
||||
for a in self.get_archives(title_id, language.legendastv):
|
||||
# clean name of path separators and pack flags
|
||||
clean_name = a.name.replace('/', '-')
|
||||
if a.pack and clean_name.startswith('(p)'):
|
||||
clean_name = clean_name[3:]
|
||||
|
||||
# guess from name
|
||||
guess = guessit(clean_name, {'type': t['type']})
|
||||
|
||||
# episode
|
||||
if season and episode:
|
||||
# discard mismatches on episode in non-pack archives
|
||||
if not a.pack and 'episode' in guess and guess['episode'] != episode:
|
||||
continue
|
||||
|
||||
# compute an expiration time based on the archive timestamp
|
||||
expiration_time = (datetime.utcnow().replace(tzinfo=pytz.utc) - a.timestamp).total_seconds()
|
||||
|
||||
# attempt to get the releases from the cache
|
||||
releases = region.get(releases_key.format(archive_id=a.id), expiration_time=expiration_time)
|
||||
|
||||
# the releases are not in cache or cache is expired
|
||||
if releases == NO_VALUE:
|
||||
logger.info('Releases not found in cache')
|
||||
|
||||
# download archive
|
||||
self.download_archive(a)
|
||||
|
||||
# extract the releases
|
||||
releases = []
|
||||
for name in a.content.namelist():
|
||||
# discard the legendastv file
|
||||
if name.startswith('Legendas.tv'):
|
||||
continue
|
||||
|
||||
# discard hidden files
|
||||
if os.path.split(name)[-1].startswith('.'):
|
||||
continue
|
||||
|
||||
# discard non-subtitle files
|
||||
if not name.lower().endswith(SUBTITLE_EXTENSIONS):
|
||||
continue
|
||||
|
||||
releases.append(name)
|
||||
|
||||
# cache the releases
|
||||
region.set(releases_key.format(archive_id=a.id), releases)
|
||||
|
||||
# iterate over releases
|
||||
for r in releases:
|
||||
subtitle = LegendasTVSubtitle(language, t['type'], t['title'], t.get('year'), t.get('imdb_id'),
|
||||
t.get('season'), a, r)
|
||||
logger.debug('Found subtitle %r', subtitle)
|
||||
subtitles.append(subtitle)
|
||||
|
||||
return subtitles
|
||||
|
||||
def list_subtitles(self, video, languages):
|
||||
season = episode = None
|
||||
if isinstance(video, Episode):
|
||||
title = video.series
|
||||
season = video.season
|
||||
episode = video.episode
|
||||
else:
|
||||
title = video.title
|
||||
|
||||
return [s for l in languages for s in self.query(l, title, season=season, episode=episode, year=video.year)]
|
||||
|
||||
def download_subtitle(self, subtitle):
|
||||
# download archive in case we previously hit the releases cache and didn't download it
|
||||
if subtitle.archive.content is None:
|
||||
self.download_archive(subtitle.archive)
|
||||
|
||||
# extract subtitle's content
|
||||
subtitle.content = fix_line_ending(subtitle.archive.content.read(subtitle.name))
|
||||
@@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
|
||||
from babelfish import Language
|
||||
from requests import Session
|
||||
|
||||
from . import Provider
|
||||
from .. import __short_version__
|
||||
from ..subtitle import Subtitle
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_subhash(hash):
|
||||
"""Get a second hash based on napiprojekt's hash.
|
||||
|
||||
:param str hash: napiprojekt's hash.
|
||||
:return: the subhash.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
idx = [0xe, 0x3, 0x6, 0x8, 0x2]
|
||||
mul = [2, 2, 5, 4, 3]
|
||||
add = [0, 0xd, 0x10, 0xb, 0x5]
|
||||
|
||||
b = []
|
||||
for i in range(len(idx)):
|
||||
a = add[i]
|
||||
m = mul[i]
|
||||
i = idx[i]
|
||||
t = a + int(hash[i], 16)
|
||||
v = int(hash[t:t + 2], 16)
|
||||
b.append(('%x' % (v * m))[-1])
|
||||
|
||||
return ''.join(b)
|
||||
|
||||
|
||||
class NapiProjektSubtitle(Subtitle):
|
||||
"""NapiProjekt Subtitle."""
|
||||
provider_name = 'napiprojekt'
|
||||
|
||||
def __init__(self, language, hash):
|
||||
super(NapiProjektSubtitle, self).__init__(language)
|
||||
self.hash = hash
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.hash
|
||||
|
||||
def get_matches(self, video):
|
||||
matches = set()
|
||||
|
||||
# hash
|
||||
if 'napiprojekt' in video.hashes and video.hashes['napiprojekt'] == self.hash:
|
||||
matches.add('hash')
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
class NapiProjektProvider(Provider):
|
||||
"""NapiProjekt Provider."""
|
||||
languages = {Language.fromalpha2(l) for l in ['pl']}
|
||||
required_hash = 'napiprojekt'
|
||||
server_url = 'http://napiprojekt.pl/unit_napisy/dl.php'
|
||||
|
||||
def initialize(self):
|
||||
self.session = Session()
|
||||
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
|
||||
|
||||
def terminate(self):
|
||||
self.session.close()
|
||||
|
||||
def query(self, language, hash):
|
||||
params = {
|
||||
'v': 'dreambox',
|
||||
'kolejka': 'false',
|
||||
'nick': '',
|
||||
'pass': '',
|
||||
'napios': 'Linux',
|
||||
'l': language.alpha2.upper(),
|
||||
'f': hash,
|
||||
't': get_subhash(hash)}
|
||||
logger.info('Searching subtitle %r', params)
|
||||
response = self.session.get(self.server_url, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
# handle subtitles not found and errors
|
||||
if response.content[:4] == b'NPc0':
|
||||
logger.debug('No subtitles found')
|
||||
return None
|
||||
|
||||
subtitle = NapiProjektSubtitle(language, hash)
|
||||
subtitle.content = response.content
|
||||
logger.debug('Found subtitle %r', subtitle)
|
||||
|
||||
return subtitle
|
||||
|
||||
def list_subtitles(self, video, languages):
|
||||
return [s for s in [self.query(l, video.hashes['napiprojekt']) for l in languages] if s is not None]
|
||||
|
||||
def download_subtitle(self, subtitle):
|
||||
# there is no download step, content is already filled from listing subtitles
|
||||
pass
|
||||
@@ -1,32 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import xmlrpclib
|
||||
import zlib
|
||||
import babelfish
|
||||
import charade
|
||||
import guessit
|
||||
from . import Provider
|
||||
from .. import __version__
|
||||
from ..exceptions import ProviderError, ProviderNotAvailable, InvalidSubtitle
|
||||
from ..subtitle import Subtitle, is_valid_subtitle, compute_guess_matches
|
||||
from ..video import Episode, Movie
|
||||
|
||||
from babelfish import Language, language_converters
|
||||
from guessit import guessit
|
||||
from six.moves.xmlrpc_client import ServerProxy
|
||||
|
||||
from . import Provider, TimeoutSafeTransport
|
||||
from .. import __short_version__
|
||||
from ..exceptions import AuthenticationError, ConfigurationError, DownloadLimitExceeded, ProviderError
|
||||
from ..subtitle import Subtitle, fix_line_ending, guess_matches
|
||||
from ..utils import sanitize
|
||||
from ..video import Episode, Movie
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenSubtitlesSubtitle(Subtitle):
|
||||
"""OpenSubtitles Subtitle."""
|
||||
provider_name = 'opensubtitles'
|
||||
series_re = re.compile('^"(?P<series_name>.*)" (?P<series_title>.*)$')
|
||||
series_re = re.compile(r'^"(?P<series_name>.*)" (?P<series_title>.*)$')
|
||||
|
||||
def __init__(self, language, hearing_impaired, id, matched_by, movie_kind, hash, movie_name, movie_release_name, # @ReservedAssignment
|
||||
movie_year, movie_imdb_id, series_season, series_episode):
|
||||
super(OpenSubtitlesSubtitle, self).__init__(language, hearing_impaired)
|
||||
self.id = id
|
||||
def __init__(self, language, hearing_impaired, page_link, subtitle_id, matched_by, movie_kind, hash, movie_name,
|
||||
movie_release_name, movie_year, movie_imdb_id, series_season, series_episode, filename, encoding):
|
||||
super(OpenSubtitlesSubtitle, self).__init__(language, hearing_impaired, page_link, encoding)
|
||||
self.subtitle_id = subtitle_id
|
||||
self.matched_by = matched_by
|
||||
self.movie_kind = movie_kind
|
||||
self.hash = hash
|
||||
@@ -36,6 +37,11 @@ class OpenSubtitlesSubtitle(Subtitle):
|
||||
self.movie_imdb_id = movie_imdb_id
|
||||
self.series_season = series_season
|
||||
self.series_episode = series_episode
|
||||
self.filename = filename
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return str(self.subtitle_id)
|
||||
|
||||
@property
|
||||
def series_name(self):
|
||||
@@ -45,115 +51,244 @@ class OpenSubtitlesSubtitle(Subtitle):
|
||||
def series_title(self):
|
||||
return self.series_re.match(self.movie_name).group('series_title')
|
||||
|
||||
def compute_matches(self, video):
|
||||
def get_matches(self, video):
|
||||
matches = set()
|
||||
|
||||
# episode
|
||||
if isinstance(video, Episode) and self.movie_kind == 'episode':
|
||||
# tag match, assume series, year, season and episode matches
|
||||
if self.matched_by == 'tag':
|
||||
matches |= {'series', 'year', 'season', 'episode'}
|
||||
# series
|
||||
if video.series and self.series_name.lower() == video.series.lower():
|
||||
if video.series and sanitize(self.series_name) == sanitize(video.series):
|
||||
matches.add('series')
|
||||
# year
|
||||
if video.original_series and self.movie_year is None or video.year and video.year == self.movie_year:
|
||||
matches.add('year')
|
||||
# season
|
||||
if video.season and self.series_season == video.season:
|
||||
matches.add('season')
|
||||
# episode
|
||||
if video.episode and self.series_episode == video.episode:
|
||||
matches.add('episode')
|
||||
# title
|
||||
if video.title and sanitize(self.series_title) == sanitize(video.title):
|
||||
matches.add('title')
|
||||
# guess
|
||||
matches |= compute_guess_matches(video, guessit.guess_episode_info(self.movie_release_name + '.mkv'))
|
||||
matches |= guess_matches(video, guessit(self.movie_release_name, {'type': 'episode'}))
|
||||
matches |= guess_matches(video, guessit(self.filename, {'type': 'episode'}))
|
||||
# hash
|
||||
if 'opensubtitles' in video.hashes and self.hash == video.hashes['opensubtitles']:
|
||||
if 'series' in matches and 'season' in matches and 'episode' in matches:
|
||||
matches.add('hash')
|
||||
else:
|
||||
logger.debug('Match on hash discarded')
|
||||
# movie
|
||||
elif isinstance(video, Movie) and self.movie_kind == 'movie':
|
||||
# tag match, assume title and year matches
|
||||
if self.matched_by == 'tag':
|
||||
matches |= {'title', 'year'}
|
||||
# title
|
||||
if video.title and sanitize(self.movie_name) == sanitize(video.title):
|
||||
matches.add('title')
|
||||
# year
|
||||
if video.year and self.movie_year == video.year:
|
||||
matches.add('year')
|
||||
# guess
|
||||
matches |= compute_guess_matches(video, guessit.guess_movie_info(self.movie_release_name + '.mkv'))
|
||||
matches |= guess_matches(video, guessit(self.movie_release_name, {'type': 'movie'}))
|
||||
matches |= guess_matches(video, guessit(self.filename, {'type': 'movie'}))
|
||||
# hash
|
||||
if 'opensubtitles' in video.hashes and self.hash == video.hashes['opensubtitles']:
|
||||
if 'title' in matches:
|
||||
matches.add('hash')
|
||||
else:
|
||||
logger.debug('Match on hash discarded')
|
||||
else:
|
||||
logger.info('%r is not a valid movie_kind for %r', self.movie_kind, video)
|
||||
logger.info('%r is not a valid movie_kind', self.movie_kind)
|
||||
return matches
|
||||
# hash
|
||||
if 'opensubtitles' in video.hashes and self.hash == video.hashes['opensubtitles']:
|
||||
matches.add('hash')
|
||||
|
||||
# imdb_id
|
||||
if video.imdb_id and self.movie_imdb_id == video.imdb_id:
|
||||
matches.add('imdb_id')
|
||||
# title
|
||||
if video.title and self.movie_name.lower() == video.title.lower():
|
||||
matches.add('title')
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
class OpenSubtitlesProvider(Provider):
|
||||
languages = {babelfish.Language.fromopensubtitles(l) for l in babelfish.get_language_converter('opensubtitles').codes}
|
||||
"""OpenSubtitles Provider.
|
||||
|
||||
def __init__(self):
|
||||
self.server = xmlrpclib.ServerProxy('http://api.opensubtitles.org/xml-rpc')
|
||||
:param str username: username.
|
||||
:param str password: password.
|
||||
|
||||
"""
|
||||
languages = {Language.fromopensubtitles(l) for l in language_converters['opensubtitles'].codes}
|
||||
|
||||
def __init__(self, username=None, password=None):
|
||||
self.server = ServerProxy('https://api.opensubtitles.org/xml-rpc', TimeoutSafeTransport(10))
|
||||
if username and not password or not username and password:
|
||||
raise ConfigurationError('Username and password must be specified')
|
||||
# None values not allowed for logging in, so replace it by ''
|
||||
self.username = username or ''
|
||||
self.password = password or ''
|
||||
self.token = None
|
||||
|
||||
def initialize(self):
|
||||
try:
|
||||
response = self.server.LogIn('', '', 'eng', 'subliminal v%s' % __version__)
|
||||
except xmlrpclib.ProtocolError:
|
||||
raise ProviderNotAvailable
|
||||
if response['status'] != '200 OK':
|
||||
raise ProviderError('Login failed with status %r' % response['status'])
|
||||
logger.info('Logging in')
|
||||
response = checked(self.server.LogIn(self.username, self.password, 'eng',
|
||||
'subliminal v%s' % __short_version__))
|
||||
self.token = response['token']
|
||||
logger.debug('Logged in with token %r', self.token)
|
||||
|
||||
def terminate(self):
|
||||
try:
|
||||
response = self.server.LogOut(self.token)
|
||||
except xmlrpclib.ProtocolError:
|
||||
raise ProviderNotAvailable
|
||||
if response['status'] != '200 OK':
|
||||
raise ProviderError('Logout failed with status %r' % response['status'])
|
||||
logger.info('Logging out')
|
||||
checked(self.server.LogOut(self.token))
|
||||
self.server.close()
|
||||
self.token = None
|
||||
logger.debug('Logged out')
|
||||
|
||||
def query(self, languages, hash=None, size=None, imdb_id=None, query=None): # @ReservedAssignment
|
||||
searches = []
|
||||
def no_operation(self):
|
||||
logger.debug('No operation')
|
||||
checked(self.server.NoOperation(self.token))
|
||||
|
||||
def query(self, languages, hash=None, size=None, imdb_id=None, query=None, season=None, episode=None, tag=None):
|
||||
# fill the search criteria
|
||||
criteria = []
|
||||
if hash and size:
|
||||
searches.append({'moviehash': hash, 'moviebytesize': str(size)})
|
||||
criteria.append({'moviehash': hash, 'moviebytesize': str(size)})
|
||||
if imdb_id:
|
||||
searches.append({'imdbid': imdb_id})
|
||||
if query:
|
||||
searches.append({'query': query})
|
||||
if not searches:
|
||||
raise ValueError('One or more parameter missing')
|
||||
for search in searches:
|
||||
search['sublanguageid'] = ','.join(l.opensubtitles for l in languages)
|
||||
logger.debug('Searching subtitles %r', searches)
|
||||
try:
|
||||
response = self.server.SearchSubtitles(self.token, searches)
|
||||
except xmlrpclib.ProtocolError:
|
||||
raise ProviderNotAvailable
|
||||
if response['status'] != '200 OK':
|
||||
raise ProviderError('Search failed with status %r' % response['status'])
|
||||
criteria.append({'imdbid': imdb_id[2:]})
|
||||
if tag:
|
||||
criteria.append({'tag': tag})
|
||||
if query and season and episode:
|
||||
criteria.append({'query': query.replace('\'', ''), 'season': season, 'episode': episode})
|
||||
elif query:
|
||||
criteria.append({'query': query.replace('\'', '')})
|
||||
if not criteria:
|
||||
raise ValueError('Not enough information')
|
||||
|
||||
# add the language
|
||||
for criterion in criteria:
|
||||
criterion['sublanguageid'] = ','.join(sorted(l.opensubtitles for l in languages))
|
||||
|
||||
# query the server
|
||||
logger.info('Searching subtitles %r', criteria)
|
||||
response = checked(self.server.SearchSubtitles(self.token, criteria))
|
||||
subtitles = []
|
||||
|
||||
# exit if no data
|
||||
if not response['data']:
|
||||
logger.debug('No subtitle found')
|
||||
return []
|
||||
return [OpenSubtitlesSubtitle(babelfish.Language.fromopensubtitles(r['SubLanguageID']),
|
||||
bool(int(r['SubHearingImpaired'])), r['IDSubtitleFile'], r['MatchedBy'],
|
||||
r['MovieKind'], r['MovieHash'], r['MovieName'], r['MovieReleaseName'],
|
||||
int(r['MovieYear']) if r['MovieYear'] else None, int(r['IDMovieImdb']),
|
||||
int(r['SeriesSeason']) if r['SeriesSeason'] else None,
|
||||
int(r['SeriesEpisode']) if r['SeriesEpisode'] else None)
|
||||
for r in response['data']]
|
||||
logger.debug('No subtitles found')
|
||||
return subtitles
|
||||
|
||||
# loop over subtitle items
|
||||
for subtitle_item in response['data']:
|
||||
# read the item
|
||||
language = Language.fromopensubtitles(subtitle_item['SubLanguageID'])
|
||||
hearing_impaired = bool(int(subtitle_item['SubHearingImpaired']))
|
||||
page_link = subtitle_item['SubtitlesLink']
|
||||
subtitle_id = int(subtitle_item['IDSubtitleFile'])
|
||||
matched_by = subtitle_item['MatchedBy']
|
||||
movie_kind = subtitle_item['MovieKind']
|
||||
hash = subtitle_item['MovieHash']
|
||||
movie_name = subtitle_item['MovieName']
|
||||
movie_release_name = subtitle_item['MovieReleaseName']
|
||||
movie_year = int(subtitle_item['MovieYear']) if subtitle_item['MovieYear'] else None
|
||||
movie_imdb_id = 'tt' + subtitle_item['IDMovieImdb']
|
||||
series_season = int(subtitle_item['SeriesSeason']) if subtitle_item['SeriesSeason'] else None
|
||||
series_episode = int(subtitle_item['SeriesEpisode']) if subtitle_item['SeriesEpisode'] else None
|
||||
filename = subtitle_item['SubFileName']
|
||||
encoding = subtitle_item.get('SubEncoding') or None
|
||||
|
||||
subtitle = OpenSubtitlesSubtitle(language, hearing_impaired, page_link, subtitle_id, matched_by, movie_kind,
|
||||
hash, movie_name, movie_release_name, movie_year, movie_imdb_id,
|
||||
series_season, series_episode, filename, encoding)
|
||||
logger.debug('Found subtitle %r by %s', subtitle, matched_by)
|
||||
subtitles.append(subtitle)
|
||||
|
||||
return subtitles
|
||||
|
||||
def list_subtitles(self, video, languages):
|
||||
query = None
|
||||
if ('opensubtitles' not in video.hashes or not video.size) and not video.imdb_id:
|
||||
query = video.name.split(os.sep)[-1]
|
||||
season = episode = None
|
||||
if isinstance(video, Episode):
|
||||
query = video.series
|
||||
season = video.season
|
||||
episode = video.episode
|
||||
else:
|
||||
query = video.title
|
||||
|
||||
return self.query(languages, hash=video.hashes.get('opensubtitles'), size=video.size, imdb_id=video.imdb_id,
|
||||
query=query)
|
||||
query=query, season=season, episode=episode, tag=os.path.basename(video.name))
|
||||
|
||||
def download_subtitle(self, subtitle):
|
||||
try:
|
||||
response = self.server.DownloadSubtitles(self.token, [subtitle.id])
|
||||
except xmlrpclib.ProtocolError:
|
||||
raise ProviderNotAvailable
|
||||
if response['status'] != '200 OK':
|
||||
raise ProviderError('Download failed with status %r' % response['status'])
|
||||
if not response['data']:
|
||||
raise ProviderError('Nothing to download')
|
||||
subtitle_bytes = zlib.decompress(base64.b64decode(response['data'][0]['data']), 47)
|
||||
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'], 'replace')
|
||||
if not is_valid_subtitle(subtitle_text):
|
||||
raise InvalidSubtitle
|
||||
return subtitle_text
|
||||
logger.info('Downloading subtitle %r', subtitle)
|
||||
response = checked(self.server.DownloadSubtitles(self.token, [str(subtitle.subtitle_id)]))
|
||||
subtitle.content = fix_line_ending(zlib.decompress(base64.b64decode(response['data'][0]['data']), 47))
|
||||
|
||||
|
||||
class OpenSubtitlesError(ProviderError):
|
||||
"""Base class for non-generic :class:`OpenSubtitlesProvider` exceptions."""
|
||||
pass
|
||||
|
||||
|
||||
class Unauthorized(OpenSubtitlesError, AuthenticationError):
|
||||
"""Exception raised when status is '401 Unauthorized'."""
|
||||
pass
|
||||
|
||||
|
||||
class NoSession(OpenSubtitlesError, AuthenticationError):
|
||||
"""Exception raised when status is '406 No session'."""
|
||||
pass
|
||||
|
||||
|
||||
class DownloadLimitReached(OpenSubtitlesError, DownloadLimitExceeded):
|
||||
"""Exception raised when status is '407 Download limit reached'."""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidImdbid(OpenSubtitlesError):
|
||||
"""Exception raised when status is '413 Invalid ImdbID'."""
|
||||
pass
|
||||
|
||||
|
||||
class UnknownUserAgent(OpenSubtitlesError, AuthenticationError):
|
||||
"""Exception raised when status is '414 Unknown User Agent'."""
|
||||
pass
|
||||
|
||||
|
||||
class DisabledUserAgent(OpenSubtitlesError, AuthenticationError):
|
||||
"""Exception raised when status is '415 Disabled user agent'."""
|
||||
pass
|
||||
|
||||
|
||||
class ServiceUnavailable(OpenSubtitlesError):
|
||||
"""Exception raised when status is '503 Service Unavailable'."""
|
||||
pass
|
||||
|
||||
|
||||
def checked(response):
|
||||
"""Check a response status before returning it.
|
||||
|
||||
:param response: a response from a XMLRPC call to OpenSubtitles.
|
||||
:return: the response.
|
||||
:raise: :class:`OpenSubtitlesError`
|
||||
|
||||
"""
|
||||
status_code = int(response['status'][:3])
|
||||
if status_code == 401:
|
||||
raise Unauthorized
|
||||
if status_code == 406:
|
||||
raise NoSession
|
||||
if status_code == 407:
|
||||
raise DownloadLimitReached
|
||||
if status_code == 413:
|
||||
raise InvalidImdbid
|
||||
if status_code == 414:
|
||||
raise UnknownUserAgent
|
||||
if status_code == 415:
|
||||
raise DisabledUserAgent
|
||||
if status_code == 503:
|
||||
raise ServiceUnavailable
|
||||
if status_code != 200:
|
||||
raise OpenSubtitlesError(response['status'])
|
||||
|
||||
return response
|
||||
|
||||
@@ -1,48 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import io
|
||||
import logging
|
||||
import re
|
||||
import xml.etree.ElementTree
|
||||
import zipfile
|
||||
import babelfish
|
||||
import bs4
|
||||
import charade
|
||||
import guessit
|
||||
import requests
|
||||
from . import Provider
|
||||
from .. import __version__
|
||||
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
|
||||
from ..subtitle import Subtitle, is_valid_subtitle, compute_guess_matches
|
||||
from ..video import Episode, Movie
|
||||
|
||||
from babelfish import Language, language_converters
|
||||
from guessit import guessit
|
||||
try:
|
||||
from lxml import etree
|
||||
except ImportError:
|
||||
try:
|
||||
import xml.etree.cElementTree as etree
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as etree
|
||||
from requests import Session
|
||||
from zipfile import ZipFile
|
||||
|
||||
from . import Provider
|
||||
from .. import __short_version__
|
||||
from ..exceptions import ProviderError
|
||||
from ..subtitle import Subtitle, fix_line_ending, guess_matches
|
||||
from ..utils import sanitize
|
||||
from ..video import Episode, Movie
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PodnapisiSubtitle(Subtitle):
|
||||
"""Podnapisi Subtitle."""
|
||||
provider_name = 'podnapisi'
|
||||
|
||||
def __init__(self, language, id, releases, hearing_impaired, link, series=None, season=None, episode=None, # @ReservedAssignment
|
||||
title=None, year=None):
|
||||
super(PodnapisiSubtitle, self).__init__(language, hearing_impaired)
|
||||
self.id = id
|
||||
def __init__(self, language, hearing_impaired, page_link, pid, releases, title, season=None, episode=None,
|
||||
year=None):
|
||||
super(PodnapisiSubtitle, self).__init__(language, hearing_impaired, page_link)
|
||||
self.pid = pid
|
||||
self.releases = releases
|
||||
self.hearing_impaired = hearing_impaired
|
||||
self.link = link
|
||||
self.series = series
|
||||
self.title = title
|
||||
self.season = season
|
||||
self.episode = episode
|
||||
self.title = title
|
||||
self.year = year
|
||||
|
||||
def compute_matches(self, video):
|
||||
@property
|
||||
def id(self):
|
||||
return self.pid
|
||||
|
||||
def get_matches(self, video):
|
||||
matches = set()
|
||||
|
||||
# episode
|
||||
if isinstance(video, Episode):
|
||||
# series
|
||||
if video.series and self.series.lower() == video.series.lower():
|
||||
if video.series and sanitize(self.title) == sanitize(video.series):
|
||||
matches.add('series')
|
||||
# year
|
||||
if video.original_series and self.year is None or video.year and video.year == self.year:
|
||||
matches.add('year')
|
||||
# season
|
||||
if video.season and self.season == video.season:
|
||||
matches.add('season')
|
||||
@@ -51,113 +62,118 @@ class PodnapisiSubtitle(Subtitle):
|
||||
matches.add('episode')
|
||||
# guess
|
||||
for release in self.releases:
|
||||
matches |= compute_guess_matches(video, guessit.guess_episode_info(release + '.mkv'))
|
||||
matches |= guess_matches(video, guessit(release, {'type': 'episode'}))
|
||||
# movie
|
||||
elif isinstance(video, Movie):
|
||||
# title
|
||||
if video.title and self.title.lower() == video.title.lower():
|
||||
if video.title and sanitize(self.title) == sanitize(video.title):
|
||||
matches.add('title')
|
||||
# year
|
||||
if video.year and self.year == video.year:
|
||||
matches.add('year')
|
||||
# guess
|
||||
for release in self.releases:
|
||||
matches |= compute_guess_matches(video, guessit.guess_movie_info(release + '.mkv'))
|
||||
matches |= guess_matches(video, guessit(release, {'type': 'movie'}))
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
class PodnapisiProvider(Provider):
|
||||
languages = {babelfish.Language.frompodnapisi(l) for l in babelfish.get_language_converter('podnapisi').codes}
|
||||
video_types = (Episode, Movie)
|
||||
server = 'http://simple.podnapisi.net'
|
||||
link_re = re.compile('^.*(?P<link>/ppodnapisi/download/i/\d+/k/.*$)')
|
||||
"""Podnapisi Provider."""
|
||||
languages = ({Language('por', 'BR'), Language('srp', script='Latn')} |
|
||||
{Language.fromalpha2(l) for l in language_converters['alpha2'].codes})
|
||||
server_url = 'http://podnapisi.net/subtitles/'
|
||||
|
||||
def initialize(self):
|
||||
self.session = requests.Session()
|
||||
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
|
||||
self.session = Session()
|
||||
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
|
||||
|
||||
def terminate(self):
|
||||
self.session.close()
|
||||
|
||||
def get(self, url, params=None, is_xml=True):
|
||||
"""Make a GET request on `url` with the given parameters
|
||||
|
||||
:param string url: part of the URL to reach with the leading slash
|
||||
:param dict params: params of the request
|
||||
:param bool xml: whether the response content is XML or not
|
||||
:return: the response
|
||||
:rtype: :class:`xml.etree.ElementTree.Element` or :class:`bs4.BeautifulSoup`
|
||||
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
|
||||
|
||||
"""
|
||||
try:
|
||||
r = self.session.get(self.server + '/ppodnapisi' + url, params=params, timeout=10)
|
||||
except requests.Timeout:
|
||||
raise ProviderNotAvailable('Timeout after 10 seconds')
|
||||
if r.status_code != 200:
|
||||
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
|
||||
if is_xml:
|
||||
return xml.etree.ElementTree.fromstring(r.content)
|
||||
else:
|
||||
return bs4.BeautifulSoup(r.content, ['permissive'])
|
||||
|
||||
def query(self, language, series=None, season=None, episode=None, title=None, year=None):
|
||||
params = {'sXML': 1, 'sJ': language.podnapisi}
|
||||
if series and season and episode:
|
||||
params['sK'] = series
|
||||
def query(self, language, keyword, season=None, episode=None, year=None):
|
||||
# set parameters, see http://www.podnapisi.net/forum/viewtopic.php?f=62&t=26164#p212652
|
||||
params = {'sXML': 1, 'sL': str(language), 'sK': keyword}
|
||||
is_episode = False
|
||||
if season and episode:
|
||||
is_episode = True
|
||||
params['sTS'] = season
|
||||
params['sTE'] = episode
|
||||
elif title:
|
||||
params['sK'] = title
|
||||
if year:
|
||||
params['sY'] = year
|
||||
else:
|
||||
raise ValueError('Missing parameters series and season and episode or title')
|
||||
logger.debug('Searching episode %r', params)
|
||||
if year:
|
||||
params['sY'] = year
|
||||
|
||||
# loop over paginated results
|
||||
logger.info('Searching subtitles %r', params)
|
||||
subtitles = []
|
||||
pids = set()
|
||||
while True:
|
||||
root = self.get('/search', params)
|
||||
if not int(root.find('pagination/results').text):
|
||||
logger.debug('No subtitle found')
|
||||
# query the server
|
||||
xml = etree.fromstring(self.session.get(self.server_url + 'search/old', params=params, timeout=10).content)
|
||||
|
||||
# exit if no results
|
||||
if not int(xml.find('pagination/results').text):
|
||||
logger.debug('No subtitles found')
|
||||
break
|
||||
if series and season and episode:
|
||||
subtitles.extend([PodnapisiSubtitle(language, int(s.find('id').text), s.find('release').text.split(),
|
||||
'h' in (s.find('flags').text or ''), s.find('url').text[38:],
|
||||
series=series, season=season, episode=episode)
|
||||
for s in root.findall('subtitle')])
|
||||
elif title:
|
||||
subtitles.extend([PodnapisiSubtitle(language, int(s.find('id').text), s.find('release').text.split(),
|
||||
'h' in (s.find('flags').text or ''), s.find('url').text[38:],
|
||||
title=title, year=year)
|
||||
for s in root.findall('subtitle')])
|
||||
if int(root.find('pagination/current').text) >= int(root.find('pagination/count').text):
|
||||
|
||||
# loop over subtitles
|
||||
for subtitle_xml in xml.findall('subtitle'):
|
||||
# read xml elements
|
||||
language = Language.fromietf(subtitle_xml.find('language').text)
|
||||
hearing_impaired = 'n' in (subtitle_xml.find('flags').text or '')
|
||||
page_link = subtitle_xml.find('url').text
|
||||
pid = subtitle_xml.find('pid').text
|
||||
releases = []
|
||||
if subtitle_xml.find('release').text:
|
||||
for release in subtitle_xml.find('release').text.split():
|
||||
release = re.sub(r'\.+$', '', release) # remove trailing dots
|
||||
release = ''.join(filter(lambda x: ord(x) < 128, release)) # remove non-ascii characters
|
||||
releases.append(release)
|
||||
title = subtitle_xml.find('title').text
|
||||
season = int(subtitle_xml.find('tvSeason').text)
|
||||
episode = int(subtitle_xml.find('tvEpisode').text)
|
||||
year = int(subtitle_xml.find('year').text)
|
||||
|
||||
if is_episode:
|
||||
subtitle = PodnapisiSubtitle(language, hearing_impaired, page_link, pid, releases, title,
|
||||
season=season, episode=episode, year=year)
|
||||
else:
|
||||
subtitle = PodnapisiSubtitle(language, hearing_impaired, page_link, pid, releases, title,
|
||||
year=year)
|
||||
|
||||
# ignore duplicates, see http://www.podnapisi.net/forum/viewtopic.php?f=62&t=26164&start=10#p213321
|
||||
if pid in pids:
|
||||
continue
|
||||
|
||||
logger.debug('Found subtitle %r', subtitle)
|
||||
subtitles.append(subtitle)
|
||||
pids.add(pid)
|
||||
|
||||
# stop on last page
|
||||
if int(xml.find('pagination/current').text) >= int(xml.find('pagination/count').text):
|
||||
break
|
||||
params['page'] = int(root.find('pagination/current').text) + 1
|
||||
|
||||
# increment current page
|
||||
params['page'] = int(xml.find('pagination/current').text) + 1
|
||||
logger.debug('Getting page %d', params['page'])
|
||||
|
||||
return subtitles
|
||||
|
||||
def list_subtitles(self, video, languages):
|
||||
if isinstance(video, Episode):
|
||||
return [s for l in languages for s in self.query(l, series=video.series, season=video.season,
|
||||
episode=video.episode)]
|
||||
return [s for l in languages for s in self.query(l, video.series, season=video.season,
|
||||
episode=video.episode, year=video.year)]
|
||||
elif isinstance(video, Movie):
|
||||
return [s for l in languages for s in self.query(l, title=video.title, year=video.year)]
|
||||
return [s for l in languages for s in self.query(l, video.title, year=video.year)]
|
||||
|
||||
def download_subtitle(self, subtitle):
|
||||
soup = self.get(subtitle.link, is_xml=False)
|
||||
link = soup.find('a', href=self.link_re)
|
||||
if not link:
|
||||
raise ProviderError('Cannot find the download link')
|
||||
try:
|
||||
r = self.session.get(self.server + self.link_re.match(link['href']).group('link'), timeout=10)
|
||||
except requests.Timeout:
|
||||
raise ProviderNotAvailable('Timeout after 10 seconds')
|
||||
if r.status_code != 200:
|
||||
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
|
||||
with zipfile.ZipFile(io.BytesIO(r.content)) as zf:
|
||||
# download as a zip
|
||||
logger.info('Downloading subtitle %r', subtitle)
|
||||
r = self.session.get(self.server_url + subtitle.pid + '/download', params={'container': 'zip'}, timeout=10)
|
||||
r.raise_for_status()
|
||||
|
||||
# open the zip
|
||||
with ZipFile(io.BytesIO(r.content)) as zf:
|
||||
if len(zf.namelist()) > 1:
|
||||
raise ProviderError('More than one file to unzip')
|
||||
subtitle_bytes = zf.read(zf.namelist()[0])
|
||||
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'], 'replace')
|
||||
if not is_valid_subtitle(subtitle_text):
|
||||
raise InvalidSubtitle
|
||||
return subtitle_text
|
||||
|
||||
subtitle.content = fix_line_ending(zf.read(zf.namelist()[0]))
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from babelfish import Language, language_converters
|
||||
from requests import Session
|
||||
|
||||
from . import Provider
|
||||
from .. import __short_version__
|
||||
from ..subtitle import Subtitle, fix_line_ending
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
language_converters.register('shooter = subliminal.converters.shooter:ShooterConverter')
|
||||
|
||||
|
||||
class ShooterSubtitle(Subtitle):
|
||||
"""Shooter Subtitle."""
|
||||
provider_name = 'shooter'
|
||||
|
||||
def __init__(self, language, hash, download_link):
|
||||
super(ShooterSubtitle, self).__init__(language)
|
||||
self.hash = hash
|
||||
self.download_link = download_link
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.download_link
|
||||
|
||||
def get_matches(self, video):
|
||||
matches = set()
|
||||
|
||||
# hash
|
||||
if 'shooter' in video.hashes and video.hashes['shooter'] == self.hash:
|
||||
matches.add('hash')
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
class ShooterProvider(Provider):
|
||||
"""Shooter Provider."""
|
||||
languages = {Language(l) for l in ['eng', 'zho']}
|
||||
server_url = 'https://www.shooter.cn/api/subapi.php'
|
||||
|
||||
def initialize(self):
|
||||
self.session = Session()
|
||||
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
|
||||
|
||||
def terminate(self):
|
||||
self.session.close()
|
||||
|
||||
def query(self, language, filename, hash=None):
|
||||
# query the server
|
||||
params = {'filehash': hash, 'pathinfo': os.path.realpath(filename), 'format': 'json', 'lang': language.shooter}
|
||||
logger.debug('Searching subtitles %r', params)
|
||||
r = self.session.post(self.server_url, params=params, timeout=10)
|
||||
r.raise_for_status()
|
||||
|
||||
# handle subtitles not found
|
||||
if r.content == b'\xff':
|
||||
logger.debug('No subtitles found')
|
||||
return []
|
||||
|
||||
# parse the subtitles
|
||||
results = json.loads(r.text)
|
||||
subtitles = [ShooterSubtitle(language, hash, t['Link']) for s in results for t in s['Files']]
|
||||
|
||||
return subtitles
|
||||
|
||||
def list_subtitles(self, video, languages):
|
||||
return [s for l in languages for s in self.query(l, video.name, video.hashes.get('shooter'))]
|
||||
|
||||
def download_subtitle(self, subtitle):
|
||||
logger.info('Downloading subtitle %r', subtitle)
|
||||
r = self.session.get(subtitle.download_link, timeout=10)
|
||||
r.raise_for_status()
|
||||
|
||||
subtitle.content = fix_line_ending(r.content)
|
||||
@@ -0,0 +1,235 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import bisect
|
||||
from collections import defaultdict
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import zipfile
|
||||
|
||||
from babelfish import Language
|
||||
from guessit import guessit
|
||||
from requests import Session
|
||||
|
||||
from . import ParserBeautifulSoup, Provider
|
||||
from .. import __short_version__
|
||||
from ..cache import SHOW_EXPIRATION_TIME, region
|
||||
from ..exceptions import AuthenticationError, ConfigurationError, ProviderError
|
||||
from ..subtitle import Subtitle, fix_line_ending, guess_matches
|
||||
from ..utils import sanitize
|
||||
from ..video import Episode, Movie
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SubsCenterSubtitle(Subtitle):
|
||||
"""SubsCenter Subtitle."""
|
||||
provider_name = 'subscenter'
|
||||
|
||||
def __init__(self, language, hearing_impaired, page_link, series, season, episode, title, subtitle_id, subtitle_key,
|
||||
downloaded, releases):
|
||||
super(SubsCenterSubtitle, self).__init__(language, hearing_impaired, page_link)
|
||||
self.series = series
|
||||
self.season = season
|
||||
self.episode = episode
|
||||
self.title = title
|
||||
self.subtitle_id = subtitle_id
|
||||
self.subtitle_key = subtitle_key
|
||||
self.downloaded = downloaded
|
||||
self.releases = releases
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return str(self.subtitle_id)
|
||||
|
||||
def get_matches(self, video):
|
||||
matches = set()
|
||||
|
||||
# episode
|
||||
if isinstance(video, Episode):
|
||||
# series
|
||||
if video.series and sanitize(self.series) == sanitize(video.series):
|
||||
matches.add('series')
|
||||
# season
|
||||
if video.season and self.season == video.season:
|
||||
matches.add('season')
|
||||
# episode
|
||||
if video.episode and self.episode == video.episode:
|
||||
matches.add('episode')
|
||||
# guess
|
||||
for release in self.releases:
|
||||
matches |= guess_matches(video, guessit(release, {'type': 'episode'}))
|
||||
# movie
|
||||
elif isinstance(video, Movie):
|
||||
# guess
|
||||
for release in self.releases:
|
||||
matches |= guess_matches(video, guessit(release, {'type': 'movie'}))
|
||||
|
||||
# title
|
||||
if video.title and sanitize(self.title) == sanitize(video.title):
|
||||
matches.add('title')
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
class SubsCenterProvider(Provider):
|
||||
"""SubsCenter Provider."""
|
||||
languages = {Language.fromalpha2(l) for l in ['he']}
|
||||
server_url = 'http://www.subscenter.co/he/'
|
||||
|
||||
def __init__(self, username=None, password=None):
|
||||
if username is not None and password is None or username is None and password is not None:
|
||||
raise ConfigurationError('Username and password must be specified')
|
||||
|
||||
self.session = None
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.logged_in = False
|
||||
|
||||
def initialize(self):
|
||||
self.session = Session()
|
||||
self.session.headers['User-Agent'] = 'Subliminal/{}'.format(__short_version__)
|
||||
|
||||
# login
|
||||
if self.username is not None and self.password is not None:
|
||||
logger.debug('Logging in')
|
||||
url = self.server_url + 'subscenter/accounts/login/'
|
||||
|
||||
# retrieve CSRF token
|
||||
self.session.get(url)
|
||||
csrf_token = self.session.cookies['csrftoken']
|
||||
|
||||
# actual login
|
||||
data = {'username': self.username, 'password': self.password, 'csrfmiddlewaretoken': csrf_token}
|
||||
r = self.session.post(url, data, allow_redirects=False, timeout=10)
|
||||
|
||||
if r.status_code != 302:
|
||||
raise AuthenticationError(self.username)
|
||||
|
||||
logger.info('Logged in')
|
||||
self.logged_in = True
|
||||
|
||||
def terminate(self):
|
||||
# logout
|
||||
if self.logged_in:
|
||||
logger.info('Logging out')
|
||||
r = self.session.get(self.server_url + 'subscenter/accounts/logout/', timeout=10)
|
||||
r.raise_for_status()
|
||||
logger.info('Logged out')
|
||||
self.logged_in = False
|
||||
|
||||
self.session.close()
|
||||
|
||||
@region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME)
|
||||
def _search_url_titles(self, title):
|
||||
"""Search the URL titles by kind for the given `title`.
|
||||
|
||||
:param str title: title to search for.
|
||||
:return: the URL titles by kind.
|
||||
:rtype: collections.defaultdict
|
||||
|
||||
"""
|
||||
# make the search
|
||||
logger.info('Searching title name for %r', title)
|
||||
r = self.session.get(self.server_url + 'subtitle/search/', params={'q': title}, timeout=10)
|
||||
r.raise_for_status()
|
||||
|
||||
# check for redirections
|
||||
if r.history and all([h.status_code == 302 for h in r.history]):
|
||||
logger.debug('Redirected to the subtitles page')
|
||||
links = [r.url]
|
||||
else:
|
||||
# get the suggestions (if needed)
|
||||
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
|
||||
links = [link.attrs['href'] for link in soup.select('#processes div.generalWindowTop a')]
|
||||
logger.debug('Found %d suggestions', len(links))
|
||||
|
||||
url_titles = defaultdict(list)
|
||||
for link in links:
|
||||
parts = link.split('/')
|
||||
url_titles[parts[-3]].append(parts[-2])
|
||||
|
||||
return url_titles
|
||||
|
||||
def query(self, title, season=None, episode=None):
|
||||
# search for the url title
|
||||
url_titles = self._search_url_titles(title)
|
||||
|
||||
# episode
|
||||
if season and episode:
|
||||
if 'series' not in url_titles:
|
||||
logger.error('No URL title found for series %r', title)
|
||||
return []
|
||||
url_title = url_titles['series'][0]
|
||||
logger.debug('Using series title %r', url_title)
|
||||
url = self.server_url + 'cst/data/series/sb/{}/{}/{}/'.format(url_title, season, episode)
|
||||
page_link = self.server_url + 'subtitle/series/{}/{}/{}/'.format(url_title, season, episode)
|
||||
else:
|
||||
if 'movie' not in url_titles:
|
||||
logger.error('No URL title found for movie %r', title)
|
||||
return []
|
||||
url_title = url_titles['movie'][0]
|
||||
logger.debug('Using movie title %r', url_title)
|
||||
url = self.server_url + 'cst/data/movie/sb/{}/'.format(url_title)
|
||||
page_link = self.server_url + 'subtitle/movie/{}/'.format(url_title)
|
||||
|
||||
# get the list of subtitles
|
||||
logger.debug('Getting the list of subtitles')
|
||||
r = self.session.get(url)
|
||||
r.raise_for_status()
|
||||
results = json.loads(r.text)
|
||||
|
||||
# loop over results
|
||||
subtitles = {}
|
||||
for language_code, language_data in results.items():
|
||||
for quality_data in language_data.values():
|
||||
for quality, subtitles_data in quality_data.items():
|
||||
for subtitle_item in subtitles_data.values():
|
||||
# read the item
|
||||
language = Language.fromalpha2(language_code)
|
||||
hearing_impaired = bool(subtitle_item['hearing_impaired'])
|
||||
subtitle_id = subtitle_item['id']
|
||||
subtitle_key = subtitle_item['key']
|
||||
downloaded = subtitle_item['downloaded']
|
||||
release = subtitle_item['subtitle_version']
|
||||
|
||||
# add the release and increment downloaded count if we already have the subtitle
|
||||
if subtitle_id in subtitles:
|
||||
logger.debug('Found additional release %r for subtitle %d', release, subtitle_id)
|
||||
bisect.insort_left(subtitles[subtitle_id].releases, release) # deterministic order
|
||||
subtitles[subtitle_id].downloaded += downloaded
|
||||
continue
|
||||
|
||||
# otherwise create it
|
||||
subtitle = SubsCenterSubtitle(language, hearing_impaired, page_link, title, season, episode,
|
||||
title, subtitle_id, subtitle_key, downloaded, [release])
|
||||
logger.debug('Found subtitle %r', subtitle)
|
||||
subtitles[subtitle_id] = subtitle
|
||||
|
||||
return subtitles.values()
|
||||
|
||||
def list_subtitles(self, video, languages):
|
||||
season = episode = None
|
||||
title = video.title
|
||||
|
||||
if isinstance(video, Episode):
|
||||
title = video.series
|
||||
season = video.season
|
||||
episode = video.episode
|
||||
|
||||
return [s for s in self.query(title, season, episode) if s.language in languages]
|
||||
|
||||
def download_subtitle(self, subtitle):
|
||||
# download
|
||||
url = self.server_url + 'subtitle/download/{}/{}/'.format(subtitle.language.alpha2, subtitle.subtitle_id)
|
||||
params = {'v': subtitle.releases[0], 'key': subtitle.subtitle_key}
|
||||
r = self.session.get(url, params=params, headers={'Referer': subtitle.page_link}, timeout=10)
|
||||
r.raise_for_status()
|
||||
|
||||
# open the zip
|
||||
with zipfile.ZipFile(io.BytesIO(r.content)) as zf:
|
||||
# remove some filenames from the namelist
|
||||
namelist = [n for n in zf.namelist() if not n.endswith('.txt')]
|
||||
if len(namelist) > 1:
|
||||
raise ProviderError('More than one file to unzip')
|
||||
|
||||
subtitle.content = fix_line_ending(zf.read(namelist[0]))
|
||||
@@ -1,81 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import logging
|
||||
import babelfish
|
||||
import charade
|
||||
import requests
|
||||
from . import Provider
|
||||
from .. import __version__
|
||||
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
|
||||
from ..subtitle import Subtitle, is_valid_subtitle
|
||||
|
||||
from babelfish import Language, language_converters
|
||||
from requests import Session
|
||||
|
||||
from . import Provider
|
||||
from .. import __short_version__
|
||||
from ..subtitle import Subtitle, fix_line_ending
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
language_converters.register('thesubdb = subliminal.converters.thesubdb:TheSubDBConverter')
|
||||
|
||||
|
||||
class TheSubDBSubtitle(Subtitle):
|
||||
"""TheSubDB Subtitle."""
|
||||
provider_name = 'thesubdb'
|
||||
|
||||
def __init__(self, language, hash): # @ReservedAssignment
|
||||
def __init__(self, language, hash):
|
||||
super(TheSubDBSubtitle, self).__init__(language)
|
||||
self.hash = hash
|
||||
|
||||
def compute_matches(self, video):
|
||||
@property
|
||||
def id(self):
|
||||
return self.hash + '-' + str(self.language)
|
||||
|
||||
def get_matches(self, video):
|
||||
matches = set()
|
||||
|
||||
# hash
|
||||
if 'thesubdb' in video.hashes and video.hashes['thesubdb'] == self.hash:
|
||||
matches.add('hash')
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
class TheSubDBProvider(Provider):
|
||||
languages = {babelfish.Language.fromalpha2(l) for l in ['en', 'es', 'fr', 'it', 'nl', 'pl', 'pt', 'ro', 'sv', 'tr']}
|
||||
"""TheSubDB Provider."""
|
||||
languages = {Language.fromthesubdb(l) for l in language_converters['thesubdb'].codes}
|
||||
required_hash = 'thesubdb'
|
||||
server_url = 'http://api.thesubdb.com/'
|
||||
|
||||
def initialize(self):
|
||||
self.session = requests.Session()
|
||||
self.session.headers = {'User-Agent': 'SubDB/1.0 (subliminal/%s; https://github.com/Diaoul/subliminal)' %
|
||||
__version__}
|
||||
self.session = Session()
|
||||
self.session.headers['User-Agent'] = ('SubDB/1.0 (subliminal/%s; https://github.com/Diaoul/subliminal)' %
|
||||
__short_version__)
|
||||
|
||||
def terminate(self):
|
||||
self.session.close()
|
||||
|
||||
def get(self, params):
|
||||
"""Make a GET request on the server with the given parameters
|
||||
|
||||
:param params: params of the request
|
||||
:return: the response
|
||||
:rtype: :class:`requests.Response`
|
||||
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
|
||||
|
||||
"""
|
||||
try:
|
||||
r = self.session.get('http://api.thesubdb.com', params=params, timeout=10)
|
||||
except requests.Timeout:
|
||||
raise ProviderNotAvailable('Timeout after 10 seconds')
|
||||
return r
|
||||
|
||||
def query(self, hash): # @ReservedAssignment
|
||||
def query(self, hash):
|
||||
# make the query
|
||||
params = {'action': 'search', 'hash': hash}
|
||||
logger.debug('Searching subtitles %r', params)
|
||||
r = self.get(params)
|
||||
logger.info('Searching subtitles %r', params)
|
||||
r = self.session.get(self.server_url, params=params, timeout=10)
|
||||
|
||||
# handle subtitles not found and errors
|
||||
if r.status_code == 404:
|
||||
logger.debug('No subtitle found')
|
||||
logger.debug('No subtitles found')
|
||||
return []
|
||||
elif r.status_code != 200:
|
||||
raise ProviderError('Request failed with status code %d' % r.status_code)
|
||||
return [TheSubDBSubtitle(language, hash) for language in
|
||||
{babelfish.Language.fromalpha2(l) for l in r.content.split(',')}]
|
||||
r.raise_for_status()
|
||||
|
||||
# loop over languages
|
||||
subtitles = []
|
||||
for language_code in r.text.split(','):
|
||||
language = Language.fromthesubdb(language_code)
|
||||
|
||||
subtitle = TheSubDBSubtitle(language, hash)
|
||||
logger.debug('Found subtitle %r', subtitle)
|
||||
subtitles.append(subtitle)
|
||||
|
||||
return subtitles
|
||||
|
||||
def list_subtitles(self, video, languages):
|
||||
return [s for s in self.query(video.hashes['thesubdb']) if s.language in languages]
|
||||
|
||||
def download_subtitle(self, subtitle):
|
||||
logger.info('Downloading subtitle %r', subtitle)
|
||||
params = {'action': 'download', 'hash': subtitle.hash, 'language': subtitle.language.alpha2}
|
||||
r = self.get(params)
|
||||
if r.status_code != 200:
|
||||
raise ProviderError('Request failed with status code %d' % r.status_code)
|
||||
subtitle_text = r.content.decode(charade.detect(r.content)['encoding'], 'replace')
|
||||
if not is_valid_subtitle(subtitle_text):
|
||||
raise InvalidSubtitle
|
||||
return subtitle_text
|
||||
r = self.session.get(self.server_url, params=params, timeout=10)
|
||||
r.raise_for_status()
|
||||
|
||||
subtitle.content = fix_line_ending(r.content)
|
||||
|
||||
+143
-108
@@ -1,40 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import io
|
||||
import logging
|
||||
import re
|
||||
import zipfile
|
||||
import babelfish
|
||||
import bs4
|
||||
import charade
|
||||
import requests
|
||||
from . import Provider
|
||||
from .. import __version__
|
||||
from ..cache import region
|
||||
from ..exceptions import InvalidSubtitle, ProviderNotAvailable, ProviderError
|
||||
from ..subtitle import Subtitle, is_valid_subtitle
|
||||
from ..video import Episode
|
||||
from zipfile import ZipFile
|
||||
|
||||
from babelfish import Language, language_converters
|
||||
from guessit import guessit
|
||||
from requests import Session
|
||||
|
||||
from . import ParserBeautifulSoup, Provider
|
||||
from .. import __short_version__
|
||||
from ..cache import EPISODE_EXPIRATION_TIME, SHOW_EXPIRATION_TIME, region
|
||||
from ..exceptions import ProviderError
|
||||
from ..score import get_equivalent_release_groups
|
||||
from ..subtitle import Subtitle, fix_line_ending, guess_matches
|
||||
from ..utils import sanitize, sanitize_release_group
|
||||
from ..video import Episode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
language_converters.register('tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter')
|
||||
|
||||
link_re = re.compile(r'^(?P<series>.+?)(?: \(?\d{4}\)?| \((?:US|UK)\))? \((?P<first_year>\d{4})-\d{4}\)$')
|
||||
episode_id_re = re.compile(r'^episode-\d+\.html$')
|
||||
|
||||
|
||||
class TVsubtitlesSubtitle(Subtitle):
|
||||
"""TVsubtitles Subtitle."""
|
||||
provider_name = 'tvsubtitles'
|
||||
|
||||
def __init__(self, language, series, season, episode, id, rip, release): # @ReservedAssignment
|
||||
super(TVsubtitlesSubtitle, self).__init__(language)
|
||||
def __init__(self, language, page_link, subtitle_id, series, season, episode, year, rip, release):
|
||||
super(TVsubtitlesSubtitle, self).__init__(language, page_link=page_link)
|
||||
self.subtitle_id = subtitle_id
|
||||
self.series = series
|
||||
self.season = season
|
||||
self.episode = episode
|
||||
self.id = id
|
||||
self.year = year
|
||||
self.rip = rip
|
||||
self.release = release
|
||||
|
||||
def compute_matches(self, video):
|
||||
@property
|
||||
def id(self):
|
||||
return str(self.subtitle_id)
|
||||
|
||||
def get_matches(self, video):
|
||||
matches = set()
|
||||
|
||||
# series
|
||||
if video.series and self.series == video.series:
|
||||
if video.series and sanitize(self.series) == sanitize(video.series):
|
||||
matches.add('series')
|
||||
# season
|
||||
if video.season and self.season == video.season:
|
||||
@@ -42,134 +55,156 @@ class TVsubtitlesSubtitle(Subtitle):
|
||||
# episode
|
||||
if video.episode and self.episode == video.episode:
|
||||
matches.add('episode')
|
||||
# year
|
||||
if video.original_series and self.year is None or video.year and video.year == self.year:
|
||||
matches.add('year')
|
||||
# release_group
|
||||
if video.release_group and self.release and video.release_group.lower() in self.release.lower():
|
||||
if (video.release_group and self.release and
|
||||
any(r in sanitize_release_group(self.release)
|
||||
for r in get_equivalent_release_groups(sanitize_release_group(video.release_group)))):
|
||||
matches.add('release_group')
|
||||
# video_codec
|
||||
if video.video_codec and self.release and (video.video_codec in self.release.lower()
|
||||
or video.video_codec == 'h264' and 'x264' in self.release.lower()):
|
||||
matches.add('video_codec')
|
||||
# resolution
|
||||
if video.resolution and self.rip and video.resolution in self.rip.lower():
|
||||
matches.add('resolution')
|
||||
# other properties
|
||||
if self.release:
|
||||
matches |= guess_matches(video, guessit(self.release, {'type': 'episode'}), partial=True)
|
||||
if self.rip:
|
||||
matches |= guess_matches(video, guessit(self.rip), partial=True)
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
class TVsubtitlesProvider(Provider):
|
||||
languages = {babelfish.Language('por', 'BR')} | {babelfish.Language(l)
|
||||
for l in ['ara', 'bul', 'ces', 'dan', 'deu', 'ell', 'eng', 'fin', 'fra', 'hun', 'ita', 'jpn', 'kor',
|
||||
'nld', 'pol', 'por', 'ron', 'rus', 'spa', 'swe', 'tur', 'ukr', 'zho']}
|
||||
"""TVsubtitles Provider."""
|
||||
languages = {Language('por', 'BR')} | {Language(l) for l in [
|
||||
'ara', 'bul', 'ces', 'dan', 'deu', 'ell', 'eng', 'fin', 'fra', 'hun', 'ita', 'jpn', 'kor', 'nld', 'pol', 'por',
|
||||
'ron', 'rus', 'spa', 'swe', 'tur', 'ukr', 'zho'
|
||||
]}
|
||||
video_types = (Episode,)
|
||||
server = 'http://www.tvsubtitles.net'
|
||||
episode_id_re = re.compile('^episode-\d+\.html$')
|
||||
subtitle_re = re.compile('^\/subtitle-\d+\.html$')
|
||||
link_re = re.compile('^(?P<series>.+) \(\d{4}-\d{4}\)$')
|
||||
server_url = 'http://www.tvsubtitles.net/'
|
||||
|
||||
def initialize(self):
|
||||
self.session = requests.Session()
|
||||
self.session.headers = {'User-Agent': 'Subliminal/%s' % __version__}
|
||||
self.session = Session()
|
||||
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
|
||||
|
||||
def terminate(self):
|
||||
self.session.close()
|
||||
|
||||
def request(self, url, params=None, data=None, method='GET'):
|
||||
"""Make a `method` request on `url` with the given parameters
|
||||
@region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME)
|
||||
def search_show_id(self, series, year=None):
|
||||
"""Search the show id from the `series` and `year`.
|
||||
|
||||
:param string url: part of the URL to reach with the leading slash
|
||||
:param dict params: params of the request
|
||||
:param dict data: data of the request
|
||||
:param string method: method of the request
|
||||
:return: the response
|
||||
:rtype: :class:`bs4.BeautifulSoup`
|
||||
:raise: :class:`~subliminal.exceptions.ProviderNotAvailable`
|
||||
:param str series: series of the episode.
|
||||
:param year: year of the series, if any.
|
||||
:type year: int
|
||||
:return: the show id, if any.
|
||||
:rtype: int
|
||||
|
||||
"""
|
||||
try:
|
||||
r = self.session.request(method, self.server + url, params=params, data=data, timeout=10)
|
||||
except requests.Timeout:
|
||||
raise ProviderNotAvailable('Timeout after 10 seconds')
|
||||
if r.status_code != 200:
|
||||
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
|
||||
return bs4.BeautifulSoup(r.content, ['permissive'])
|
||||
# make the search
|
||||
logger.info('Searching show id for %r', series)
|
||||
r = self.session.post(self.server_url + 'search.php', data={'q': series}, timeout=10)
|
||||
r.raise_for_status()
|
||||
|
||||
@region.cache_on_arguments()
|
||||
def find_show_id(self, series):
|
||||
"""Find a show id from the series
|
||||
|
||||
:param string series: series of the episode
|
||||
:return: the show id, if any
|
||||
:rtype: int or None
|
||||
|
||||
"""
|
||||
data = {'q': series}
|
||||
logger.debug('Searching series %r', data)
|
||||
soup = self.request('/search.php', data=data, method='POST')
|
||||
links = soup.select('div.left li div a[href^="/tvshow-"]')
|
||||
if not links:
|
||||
logger.info('Series %r not found', series)
|
||||
return None
|
||||
for link in links:
|
||||
match = self.link_re.match(link.string)
|
||||
# get the series out of the suggestions
|
||||
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
|
||||
show_id = None
|
||||
for suggestion in soup.select('div.left li div a[href^="/tvshow-"]'):
|
||||
match = link_re.match(suggestion.text)
|
||||
if not match:
|
||||
logger.warning('Could not parse %r', link.string)
|
||||
logger.error('Failed to match %s', suggestion.text)
|
||||
continue
|
||||
if match.group('series').lower().replace('.', ' ').strip() == series.lower():
|
||||
return int(link['href'][8:-5])
|
||||
return int(links[0]['href'][8:-5])
|
||||
|
||||
@region.cache_on_arguments()
|
||||
def find_episode_ids(self, show_id, season):
|
||||
"""Find episode ids from the show id and the season
|
||||
if match.group('series').lower() == series.lower():
|
||||
if year is not None and int(match.group('first_year')) != year:
|
||||
logger.debug('Year does not match')
|
||||
continue
|
||||
show_id = int(suggestion['href'][8:-5])
|
||||
logger.debug('Found show id %d', show_id)
|
||||
break
|
||||
|
||||
:param int show_id: show id
|
||||
:param int season: season of the episode
|
||||
:return: episode ids per episode number
|
||||
return show_id
|
||||
|
||||
@region.cache_on_arguments(expiration_time=EPISODE_EXPIRATION_TIME)
|
||||
def get_episode_ids(self, show_id, season):
|
||||
"""Get episode ids from the show id and the season.
|
||||
|
||||
:param int show_id: show id.
|
||||
:param int season: season of the episode.
|
||||
:return: episode ids per episode number.
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
params = {'show_id': show_id, 'season': season}
|
||||
logger.debug('Searching episodes %r', params)
|
||||
soup = self.request('/tvshow-{show_id}-{season}.html'.format(**params))
|
||||
# get the page of the season of the show
|
||||
logger.info('Getting the page of show id %d, season %d', show_id, season)
|
||||
r = self.session.get(self.server_url + 'tvshow-%d-%d.html' % (show_id, season), timeout=10)
|
||||
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
|
||||
|
||||
# loop over episode rows
|
||||
episode_ids = {}
|
||||
for row in soup.select('table#table5 tr'):
|
||||
if not row('a', href=self.episode_id_re):
|
||||
# skip rows that do not have a link to the episode page
|
||||
if not row('a', href=episode_id_re):
|
||||
continue
|
||||
|
||||
# extract data from the cells
|
||||
cells = row('td')
|
||||
episode_ids[int(cells[0].string.split('x')[1])] = int(cells[1].a['href'][8:-5])
|
||||
episode = int(cells[0].text.split('x')[1])
|
||||
episode_id = int(cells[1].a['href'][8:-5])
|
||||
episode_ids[episode] = episode_id
|
||||
|
||||
if episode_ids:
|
||||
logger.debug('Found episode ids %r', episode_ids)
|
||||
else:
|
||||
logger.warning('No episode ids found')
|
||||
|
||||
return episode_ids
|
||||
|
||||
def query(self, series, season, episode):
|
||||
show_id = self.find_show_id(series.lower())
|
||||
def query(self, series, season, episode, year=None):
|
||||
# search the show id
|
||||
show_id = self.search_show_id(series, year)
|
||||
if show_id is None:
|
||||
logger.error('No show id found for %r (%r)', series, {'year': year})
|
||||
return []
|
||||
episode_ids = self.find_episode_ids(show_id, season)
|
||||
|
||||
# get the episode ids
|
||||
episode_ids = self.get_episode_ids(show_id, season)
|
||||
if episode not in episode_ids:
|
||||
logger.info('Episode %d not found', episode)
|
||||
logger.error('Episode %d not found', episode)
|
||||
return []
|
||||
params = {'episode_id': episode_ids[episode]}
|
||||
logger.debug('Searching episode %r', params)
|
||||
soup = self.request('/episode-{episode_id}.html'.format(**params))
|
||||
return [TVsubtitlesSubtitle(babelfish.Language.fromtvsubtitles(row.h5.img['src'][13:-4]), series, season,
|
||||
episode, row['href'][10:-5], row.find('p', title='rip').text.strip() or None,
|
||||
row.find('p', title='release').text.strip() or None)
|
||||
for row in soup('a', href=self.subtitle_re)]
|
||||
|
||||
# get the episode page
|
||||
logger.info('Getting the page for episode %d', episode_ids[episode])
|
||||
r = self.session.get(self.server_url + 'episode-%d.html' % episode_ids[episode], timeout=10)
|
||||
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
|
||||
|
||||
# loop over subtitles rows
|
||||
subtitles = []
|
||||
for row in soup.select('.subtitlen'):
|
||||
# read the item
|
||||
language = Language.fromtvsubtitles(row.h5.img['src'][13:-4])
|
||||
subtitle_id = int(row.parent['href'][10:-5])
|
||||
page_link = self.server_url + 'subtitle-%d.html' % subtitle_id
|
||||
rip = row.find('p', title='rip').text.strip() or None
|
||||
release = row.find('p', title='release').text.strip() or None
|
||||
|
||||
subtitle = TVsubtitlesSubtitle(language, page_link, subtitle_id, series, season, episode, year, rip,
|
||||
release)
|
||||
logger.debug('Found subtitle %s', subtitle)
|
||||
subtitles.append(subtitle)
|
||||
|
||||
return subtitles
|
||||
|
||||
def list_subtitles(self, video, languages):
|
||||
return [s for s in self.query(video.series, video.season, video.episode) if s.language in languages]
|
||||
return [s for s in self.query(video.series, video.season, video.episode, video.year) if s.language in languages]
|
||||
|
||||
def download_subtitle(self, subtitle):
|
||||
try:
|
||||
r = self.session.get(self.server + '/download-{subtitle_id}.html'.format(subtitle_id=subtitle.id),
|
||||
timeout=10)
|
||||
except requests.Timeout:
|
||||
raise ProviderNotAvailable('Timeout after 10 seconds')
|
||||
if r.status_code != 200:
|
||||
raise ProviderNotAvailable('Request failed with status code %d' % r.status_code)
|
||||
with zipfile.ZipFile(io.BytesIO(r.content)) as zf:
|
||||
# download as a zip
|
||||
logger.info('Downloading subtitle %r', subtitle)
|
||||
r = self.session.get(self.server_url + 'download-%d.html' % subtitle.subtitle_id, timeout=10)
|
||||
r.raise_for_status()
|
||||
|
||||
# open the zip
|
||||
with ZipFile(io.BytesIO(r.content)) as zf:
|
||||
if len(zf.namelist()) > 1:
|
||||
raise ProviderError('More than one file to unzip')
|
||||
subtitle_bytes = zf.read(zf.namelist()[0])
|
||||
subtitle_text = subtitle_bytes.decode(charade.detect(subtitle_bytes)['encoding'], 'replace')
|
||||
if not is_valid_subtitle(subtitle_text):
|
||||
raise InvalidSubtitle
|
||||
return subtitle_text
|
||||
|
||||
subtitle.content = fix_line_ending(zf.read(zf.namelist()[0]))
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Refiners enrich a :class:`~subliminal.video.Video` object by adding information to it.
|
||||
|
||||
A refiner is a simple function:
|
||||
|
||||
.. py:function:: refine(video, **kwargs)
|
||||
|
||||
:param video: the video to refine.
|
||||
:type video: :class:`~subliminal.video.Video`
|
||||
:param \*\*kwargs: additional parameters for refiners.
|
||||
|
||||
"""
|
||||
@@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import os
|
||||
|
||||
from babelfish import Error as BabelfishError, Language
|
||||
from enzyme import MKV
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def refine(video, embedded_subtitles=True, **kwargs):
|
||||
"""Refine a video by searching its metadata.
|
||||
|
||||
Several :class:`~subliminal.video.Video` attributes can be found:
|
||||
|
||||
* :attr:`~subliminal.video.Video.resolution`
|
||||
* :attr:`~subliminal.video.Video.video_codec`
|
||||
* :attr:`~subliminal.video.Video.audio_codec`
|
||||
* :attr:`~subliminal.video.Video.subtitle_languages`
|
||||
|
||||
:param bool embedded_subtitles: search for embedded subtitles.
|
||||
|
||||
"""
|
||||
# skip non existing videos
|
||||
if not video.exists:
|
||||
return
|
||||
|
||||
# check extensions
|
||||
extension = os.path.splitext(video.name)[1]
|
||||
if extension == '.mkv':
|
||||
with open(video.name, 'rb') as f:
|
||||
mkv = MKV(f)
|
||||
|
||||
# main video track
|
||||
if mkv.video_tracks:
|
||||
video_track = mkv.video_tracks[0]
|
||||
|
||||
# resolution
|
||||
if video_track.height in (480, 720, 1080):
|
||||
if video_track.interlaced:
|
||||
video.resolution = '%di' % video_track.height
|
||||
else:
|
||||
video.resolution = '%dp' % video_track.height
|
||||
logger.debug('Found resolution %s', video.resolution)
|
||||
|
||||
# video codec
|
||||
if video_track.codec_id == 'V_MPEG4/ISO/AVC':
|
||||
video.video_codec = 'h264'
|
||||
logger.debug('Found video_codec %s', video.video_codec)
|
||||
elif video_track.codec_id == 'V_MPEG4/ISO/SP':
|
||||
video.video_codec = 'DivX'
|
||||
logger.debug('Found video_codec %s', video.video_codec)
|
||||
elif video_track.codec_id == 'V_MPEG4/ISO/ASP':
|
||||
video.video_codec = 'XviD'
|
||||
logger.debug('Found video_codec %s', video.video_codec)
|
||||
else:
|
||||
logger.warning('MKV has no video track')
|
||||
|
||||
# main audio track
|
||||
if mkv.audio_tracks:
|
||||
audio_track = mkv.audio_tracks[0]
|
||||
# audio codec
|
||||
if audio_track.codec_id == 'A_AC3':
|
||||
video.audio_codec = 'AC3'
|
||||
logger.debug('Found audio_codec %s', video.audio_codec)
|
||||
elif audio_track.codec_id == 'A_DTS':
|
||||
video.audio_codec = 'DTS'
|
||||
logger.debug('Found audio_codec %s', video.audio_codec)
|
||||
elif audio_track.codec_id == 'A_AAC':
|
||||
video.audio_codec = 'AAC'
|
||||
logger.debug('Found audio_codec %s', video.audio_codec)
|
||||
else:
|
||||
logger.warning('MKV has no audio track')
|
||||
|
||||
# subtitle tracks
|
||||
if mkv.subtitle_tracks:
|
||||
if embedded_subtitles:
|
||||
embedded_subtitle_languages = set()
|
||||
for st in mkv.subtitle_tracks:
|
||||
if st.language:
|
||||
try:
|
||||
embedded_subtitle_languages.add(Language.fromalpha3b(st.language))
|
||||
except BabelfishError:
|
||||
logger.error('Embedded subtitle track language %r is not a valid language', st.language)
|
||||
embedded_subtitle_languages.add(Language('und'))
|
||||
elif st.name:
|
||||
try:
|
||||
embedded_subtitle_languages.add(Language.fromname(st.name))
|
||||
except BabelfishError:
|
||||
logger.debug('Embedded subtitle track name %r is not a valid language', st.name)
|
||||
embedded_subtitle_languages.add(Language('und'))
|
||||
else:
|
||||
embedded_subtitle_languages.add(Language('und'))
|
||||
logger.debug('Found embedded subtitle %r', embedded_subtitle_languages)
|
||||
video.subtitle_languages |= embedded_subtitle_languages
|
||||
else:
|
||||
logger.debug('MKV has no subtitle track')
|
||||
else:
|
||||
logger.debug('Unsupported video extension %s', extension)
|
||||
@@ -0,0 +1,187 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import operator
|
||||
|
||||
import requests
|
||||
|
||||
from .. import __short_version__
|
||||
from ..cache import REFINER_EXPIRATION_TIME, region
|
||||
from ..video import Episode, Movie
|
||||
from ..utils import sanitize
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OMDBClient(object):
|
||||
base_url = 'http://www.omdbapi.com'
|
||||
|
||||
def __init__(self, version=1, session=None, headers=None, timeout=10):
|
||||
#: Session for the requests
|
||||
self.session = session or requests.Session()
|
||||
self.session.timeout = timeout
|
||||
self.session.headers.update(headers or {})
|
||||
self.session.params['r'] = 'json'
|
||||
self.session.params['v'] = version
|
||||
|
||||
def get(self, id=None, title=None, type=None, year=None, plot='short', tomatoes=False):
|
||||
# build the params
|
||||
params = {}
|
||||
if id:
|
||||
params['i'] = id
|
||||
if title:
|
||||
params['t'] = title
|
||||
if not params:
|
||||
raise ValueError('At least id or title is required')
|
||||
params['type'] = type
|
||||
params['y'] = year
|
||||
params['plot'] = plot
|
||||
params['tomatoes'] = tomatoes
|
||||
|
||||
# perform the request
|
||||
r = self.session.get(self.base_url, params=params)
|
||||
r.raise_for_status()
|
||||
|
||||
# get the response as json
|
||||
j = r.json()
|
||||
|
||||
# check response status
|
||||
if j['Response'] == 'False':
|
||||
return None
|
||||
|
||||
return j
|
||||
|
||||
def search(self, title, type=None, year=None, page=1):
|
||||
# build the params
|
||||
params = {'s': title, 'type': type, 'y': year, 'page': page}
|
||||
|
||||
# perform the request
|
||||
r = self.session.get(self.base_url, params=params)
|
||||
r.raise_for_status()
|
||||
|
||||
# get the response as json
|
||||
j = r.json()
|
||||
|
||||
# check response status
|
||||
if j['Response'] == 'False':
|
||||
return None
|
||||
|
||||
return j
|
||||
|
||||
|
||||
omdb_client = OMDBClient(headers={'User-Agent': 'Subliminal/%s' % __short_version__})
|
||||
|
||||
|
||||
@region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME)
|
||||
def search(title, type, year):
|
||||
results = omdb_client.search(title, type, year)
|
||||
if not results:
|
||||
return None
|
||||
|
||||
# fetch all paginated results
|
||||
all_results = results['Search']
|
||||
total_results = int(results['totalResults'])
|
||||
page = 1
|
||||
while total_results > page * 10:
|
||||
page += 1
|
||||
results = omdb_client.search(title, type, year, page=page)
|
||||
all_results.extend(results['Search'])
|
||||
|
||||
return all_results
|
||||
|
||||
|
||||
def refine(video, **kwargs):
|
||||
"""Refine a video by searching `OMDb API <http://omdbapi.com/>`_.
|
||||
|
||||
Several :class:`~subliminal.video.Episode` attributes can be found:
|
||||
|
||||
* :attr:`~subliminal.video.Episode.series`
|
||||
* :attr:`~subliminal.video.Episode.year`
|
||||
* :attr:`~subliminal.video.Episode.series_imdb_id`
|
||||
|
||||
Similarly, for a :class:`~subliminal.video.Movie`:
|
||||
|
||||
* :attr:`~subliminal.video.Movie.title`
|
||||
* :attr:`~subliminal.video.Movie.year`
|
||||
* :attr:`~subliminal.video.Video.imdb_id`
|
||||
|
||||
"""
|
||||
if isinstance(video, Episode):
|
||||
# exit if the information is complete
|
||||
if video.series_imdb_id:
|
||||
logger.debug('No need to search')
|
||||
return
|
||||
|
||||
# search the series
|
||||
results = search(video.series, 'series', video.year)
|
||||
if not results:
|
||||
logger.warning('No results for series')
|
||||
return
|
||||
logger.debug('Found %d results', len(results))
|
||||
|
||||
# filter the results
|
||||
results = [r for r in results if sanitize(r['Title']) == sanitize(video.series)]
|
||||
if not results:
|
||||
logger.warning('No matching series found')
|
||||
return
|
||||
|
||||
# process the results
|
||||
found = False
|
||||
for result in sorted(results, key=operator.itemgetter('Year')):
|
||||
if video.original_series and video.year is None:
|
||||
logger.debug('Found result for original series without year')
|
||||
found = True
|
||||
break
|
||||
if video.year == int(result['Year'].split(u'\u2013')[0]):
|
||||
logger.debug('Found result with matching year')
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
logger.warning('No matching series found')
|
||||
return
|
||||
|
||||
# add series information
|
||||
logger.debug('Found series %r', result)
|
||||
video.series = result['Title']
|
||||
video.year = int(result['Year'].split(u'\u2013')[0])
|
||||
video.series_imdb_id = result['imdbID']
|
||||
|
||||
elif isinstance(video, Movie):
|
||||
# exit if the information is complete
|
||||
if video.imdb_id:
|
||||
return
|
||||
|
||||
# search the movie
|
||||
results = search(video.title, 'movie', video.year)
|
||||
if not results:
|
||||
logger.warning('No results')
|
||||
return
|
||||
logger.debug('Found %d results', len(results))
|
||||
|
||||
# filter the results
|
||||
results = [r for r in results if sanitize(r['Title']) == sanitize(video.title)]
|
||||
if not results:
|
||||
logger.warning('No matching movie found')
|
||||
return
|
||||
|
||||
# process the results
|
||||
found = False
|
||||
for result in results:
|
||||
if video.year is None:
|
||||
logger.debug('Found result for movie without year')
|
||||
found = True
|
||||
break
|
||||
if video.year == int(result['Year']):
|
||||
logger.debug('Found result with matching year')
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
logger.warning('No matching movie found')
|
||||
return
|
||||
|
||||
# add movie information
|
||||
logger.debug('Found movie %r', result)
|
||||
video.title = result['Title']
|
||||
video.year = int(result['Year'].split(u'\u2013')[0])
|
||||
video.imdb_id = result['imdbID']
|
||||
@@ -0,0 +1,350 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
import logging
|
||||
import re
|
||||
|
||||
import requests
|
||||
|
||||
from .. import __short_version__
|
||||
from ..cache import REFINER_EXPIRATION_TIME, region
|
||||
from ..utils import sanitize
|
||||
from ..video import Episode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
series_re = re.compile(r'^(?P<series>.*?)(?: \((?:(?P<year>\d{4})|(?P<country>[A-Z]{2}))\))?$')
|
||||
|
||||
|
||||
def requires_auth(func):
|
||||
"""Decorator for :class:`TVDBClient` methods that require authentication"""
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if self.token is None or self.token_expired:
|
||||
self.login()
|
||||
elif self.token_needs_refresh:
|
||||
self.refresh_token()
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class TVDBClient(object):
|
||||
"""TVDB REST API Client
|
||||
|
||||
:param str apikey: API key to use.
|
||||
:param str username: username to use.
|
||||
:param str password: password to use.
|
||||
:param str language: language of the responses.
|
||||
:param session: session object to use.
|
||||
:type session: :class:`requests.sessions.Session` or compatible.
|
||||
:param dict headers: additional headers.
|
||||
:param int timeout: timeout for the requests.
|
||||
|
||||
"""
|
||||
#: Base URL of the API
|
||||
base_url = 'https://api.thetvdb.com'
|
||||
|
||||
#: Token lifespan
|
||||
token_lifespan = timedelta(hours=1)
|
||||
|
||||
#: Minimum token age before a :meth:`refresh_token` is triggered
|
||||
refresh_token_every = timedelta(minutes=30)
|
||||
|
||||
def __init__(self, apikey=None, username=None, password=None, language='en', session=None, headers=None,
|
||||
timeout=10):
|
||||
#: API key
|
||||
self.apikey = apikey
|
||||
|
||||
#: Username
|
||||
self.username = username
|
||||
|
||||
#: Password
|
||||
self.password = password
|
||||
|
||||
#: Last token acquisition date
|
||||
self.token_date = datetime.utcnow() - self.token_lifespan
|
||||
|
||||
#: Session for the requests
|
||||
self.session = session or requests.Session()
|
||||
self.session.timeout = timeout
|
||||
self.session.headers.update(headers or {})
|
||||
self.session.headers['Content-Type'] = 'application/json'
|
||||
self.session.headers['Accept-Language'] = language
|
||||
|
||||
@property
|
||||
def language(self):
|
||||
return self.session.headers['Accept-Language']
|
||||
|
||||
@language.setter
|
||||
def language(self, value):
|
||||
self.session.headers['Accept-Language'] = value
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
if 'Authorization' not in self.session.headers:
|
||||
return None
|
||||
return self.session.headers['Authorization'][7:]
|
||||
|
||||
@property
|
||||
def token_expired(self):
|
||||
return datetime.utcnow() - self.token_date > self.token_lifespan
|
||||
|
||||
@property
|
||||
def token_needs_refresh(self):
|
||||
return datetime.utcnow() - self.token_date > self.refresh_token_every
|
||||
|
||||
def login(self):
|
||||
"""Login"""
|
||||
# perform the request
|
||||
data = {'apikey': self.apikey, 'username': self.username, 'password': self.password}
|
||||
r = self.session.post(self.base_url + '/login', json=data)
|
||||
r.raise_for_status()
|
||||
|
||||
# set the Authorization header
|
||||
self.session.headers['Authorization'] = 'Bearer ' + r.json()['token']
|
||||
|
||||
# update token_date
|
||||
self.token_date = datetime.utcnow()
|
||||
|
||||
def refresh_token(self):
|
||||
"""Refresh token"""
|
||||
# perform the request
|
||||
r = self.session.get(self.base_url + '/refresh_token')
|
||||
r.raise_for_status()
|
||||
|
||||
# set the Authorization header
|
||||
self.session.headers['Authorization'] = 'Bearer ' + r.json()['token']
|
||||
|
||||
# update token_date
|
||||
self.token_date = datetime.utcnow()
|
||||
|
||||
@requires_auth
|
||||
def search_series(self, name=None, imdb_id=None, zap2it_id=None):
|
||||
"""Search series"""
|
||||
# perform the request
|
||||
params = {'name': name, 'imdbId': imdb_id, 'zap2itId': zap2it_id}
|
||||
r = self.session.get(self.base_url + '/search/series', params=params)
|
||||
if r.status_code == 404:
|
||||
return None
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()['data']
|
||||
|
||||
@requires_auth
|
||||
def get_series(self, id):
|
||||
"""Get series"""
|
||||
# perform the request
|
||||
r = self.session.get(self.base_url + '/series/{}'.format(id))
|
||||
if r.status_code == 404:
|
||||
return None
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()['data']
|
||||
|
||||
@requires_auth
|
||||
def get_series_actors(self, id):
|
||||
"""Get series actors"""
|
||||
# perform the request
|
||||
r = self.session.get(self.base_url + '/series/{}/actors'.format(id))
|
||||
if r.status_code == 404:
|
||||
return None
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()['data']
|
||||
|
||||
@requires_auth
|
||||
def get_series_episodes(self, id, page=1):
|
||||
"""Get series episodes"""
|
||||
# perform the request
|
||||
params = {'page': page}
|
||||
r = self.session.get(self.base_url + '/series/{}/episodes'.format(id), params=params)
|
||||
if r.status_code == 404:
|
||||
return None
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()
|
||||
|
||||
@requires_auth
|
||||
def query_series_episodes(self, id, absolute_number=None, aired_season=None, aired_episode=None, dvd_season=None,
|
||||
dvd_episode=None, imdb_id=None, page=1):
|
||||
"""Query series episodes"""
|
||||
# perform the request
|
||||
params = {'absoluteNumber': absolute_number, 'airedSeason': aired_season, 'airedEpisode': aired_episode,
|
||||
'dvdSeason': dvd_season, 'dvdEpisode': dvd_episode, 'imdbId': imdb_id, 'page': page}
|
||||
r = self.session.get(self.base_url + '/series/{}/episodes/query'.format(id), params=params)
|
||||
if r.status_code == 404:
|
||||
return None
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()
|
||||
|
||||
@requires_auth
|
||||
def get_episode(self, id):
|
||||
"""Get episode"""
|
||||
# perform the request
|
||||
r = self.session.get(self.base_url + '/episodes/{}'.format(id))
|
||||
if r.status_code == 404:
|
||||
return None
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()['data']
|
||||
|
||||
|
||||
#: Configured instance of :class:`TVDBClient`
|
||||
tvdb_client = TVDBClient('5EC930FB90DA1ADA', headers={'User-Agent': 'Subliminal/%s' % __short_version__})
|
||||
|
||||
|
||||
@region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME)
|
||||
def search_series(name):
|
||||
"""Search series.
|
||||
|
||||
:param str name: name of the series.
|
||||
:return: the search results.
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
return tvdb_client.search_series(name)
|
||||
|
||||
|
||||
@region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME)
|
||||
def get_series(id):
|
||||
"""Get series.
|
||||
|
||||
:param int id: id of the series.
|
||||
:return: the series data.
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
return tvdb_client.get_series(id)
|
||||
|
||||
|
||||
@region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME)
|
||||
def get_series_episode(series_id, season, episode):
|
||||
"""Get an episode of a series.
|
||||
|
||||
:param int series_id: id of the series.
|
||||
:param int season: season number of the episode.
|
||||
:param int episode: episode number of the episode.
|
||||
:return: the episode data.
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
result = tvdb_client.query_series_episodes(series_id, aired_season=season, aired_episode=episode)
|
||||
if result:
|
||||
return tvdb_client.get_episode(result['data'][0]['id'])
|
||||
|
||||
|
||||
def refine(video, **kwargs):
|
||||
"""Refine a video by searching `TheTVDB <http://thetvdb.com/>`_.
|
||||
|
||||
.. note::
|
||||
|
||||
This refiner only work for instances of :class:`~subliminal.video.Episode`.
|
||||
|
||||
Several attributes can be found:
|
||||
|
||||
* :attr:`~subliminal.video.Episode.series`
|
||||
* :attr:`~subliminal.video.Episode.year`
|
||||
* :attr:`~subliminal.video.Episode.series_imdb_id`
|
||||
* :attr:`~subliminal.video.Episode.series_tvdb_id`
|
||||
* :attr:`~subliminal.video.Episode.title`
|
||||
* :attr:`~subliminal.video.Video.imdb_id`
|
||||
* :attr:`~subliminal.video.Episode.tvdb_id`
|
||||
|
||||
"""
|
||||
# only deal with Episode videos
|
||||
if not isinstance(video, Episode):
|
||||
logger.error('Cannot refine episodes')
|
||||
return
|
||||
|
||||
# exit if the information is complete
|
||||
if video.series_tvdb_id and video.tvdb_id:
|
||||
logger.debug('No need to search')
|
||||
return
|
||||
|
||||
# search the series
|
||||
logger.info('Searching series %r', video.series)
|
||||
results = search_series(video.series.lower())
|
||||
if not results:
|
||||
logger.warning('No results for series')
|
||||
return
|
||||
logger.debug('Found %d results', len(results))
|
||||
|
||||
# search for exact matches
|
||||
matching_results = []
|
||||
for result in results:
|
||||
matching_result = {}
|
||||
|
||||
# use seriesName and aliases
|
||||
series_names = [result['seriesName']]
|
||||
series_names.extend(result['aliases'])
|
||||
|
||||
# parse the original series as series + year or country
|
||||
original_match = series_re.match(result['seriesName']).groupdict()
|
||||
|
||||
# parse series year
|
||||
series_year = None
|
||||
if result['firstAired']:
|
||||
series_year = datetime.strptime(result['firstAired'], '%Y-%m-%d').year
|
||||
|
||||
# discard mismatches on year
|
||||
if video.year and series_year and video.year != series_year:
|
||||
logger.debug('Discarding series %r mismatch on year %d', result['seriesName'], series_year)
|
||||
continue
|
||||
|
||||
# iterate over series names
|
||||
for series_name in series_names:
|
||||
# parse as series and year
|
||||
series, year, country = series_re.match(series_name).groups()
|
||||
if year:
|
||||
year = int(year)
|
||||
|
||||
# discard mismatches on year
|
||||
if year and (video.original_series or video.year != year):
|
||||
logger.debug('Discarding series name %r mismatch on year %d', series, year)
|
||||
continue
|
||||
|
||||
# match on sanitized series name
|
||||
if sanitize(series) == sanitize(video.series):
|
||||
logger.debug('Found exact match on series %r', series_name)
|
||||
matching_result['match'] = {'series': original_match['series'], 'year': series_year,
|
||||
'original_series': original_match['year'] is None}
|
||||
break
|
||||
|
||||
# add the result on match
|
||||
if matching_result:
|
||||
matching_result['data'] = result
|
||||
matching_results.append(matching_result)
|
||||
|
||||
# exit if we don't have exactly 1 matching result
|
||||
if not matching_results:
|
||||
logger.error('No matching series found')
|
||||
return
|
||||
if len(matching_results) > 1:
|
||||
logger.error('Multiple matches found')
|
||||
return
|
||||
|
||||
# get the series
|
||||
matching_result = matching_results[0]
|
||||
series = get_series(matching_result['data']['id'])
|
||||
|
||||
# add series information
|
||||
logger.debug('Found series %r', series)
|
||||
video.series = matching_result['match']['series']
|
||||
video.year = matching_result['match']['year']
|
||||
video.original_series = matching_result['match']['original_series']
|
||||
video.series_tvdb_id = series['id']
|
||||
video.series_imdb_id = series['imdbId'] or None
|
||||
|
||||
# get the episode
|
||||
logger.info('Getting series episode %dx%d', video.season, video.episode)
|
||||
episode = get_series_episode(video.series_tvdb_id, video.season, video.episode)
|
||||
if not episode:
|
||||
logger.warning('No results for episode')
|
||||
return
|
||||
|
||||
# add episode information
|
||||
logger.debug('Found episode %r', episode)
|
||||
video.tvdb_id = episode['id']
|
||||
video.title = episode['episodeName'] or None
|
||||
video.imdb_id = episode['imdbId'] or None
|
||||
+218
-68
@@ -1,84 +1,234 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import print_function, unicode_literals
|
||||
from sympy import Eq, symbols, solve
|
||||
"""
|
||||
This module provides the default implementation of the `compute_score` parameter in
|
||||
:meth:`~subliminal.core.ProviderPool.download_best_subtitles` and :func:`~subliminal.core.download_best_subtitles`.
|
||||
|
||||
.. note::
|
||||
|
||||
To avoid unnecessary dependency on `sympy <http://www.sympy.org/>`_ and boost subliminal's import time, the
|
||||
resulting scores are hardcoded here and manually updated when the set of equations change.
|
||||
|
||||
Available matches:
|
||||
|
||||
* hash
|
||||
* title
|
||||
* year
|
||||
* series
|
||||
* season
|
||||
* episode
|
||||
* release_group
|
||||
* format
|
||||
* audio_codec
|
||||
* resolution
|
||||
* hearing_impaired
|
||||
* video_codec
|
||||
* series_imdb_id
|
||||
* imdb_id
|
||||
* tvdb_id
|
||||
|
||||
"""
|
||||
from __future__ import division, print_function
|
||||
import logging
|
||||
|
||||
from .video import Episode, Movie
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Symbols
|
||||
release_group, resolution, video_codec, audio_codec = symbols('release_group resolution video_codec audio_codec')
|
||||
imdb_id, hash, title, series, tvdb_id, season, episode = symbols('imdb_id hash title series tvdb_id season episode') # @ReservedAssignment
|
||||
year = symbols('year')
|
||||
#: Scores for episodes
|
||||
episode_scores = {'hash': 359, 'series': 180, 'year': 90, 'season': 30, 'episode': 30, 'release_group': 15,
|
||||
'format': 7, 'audio_codec': 3, 'resolution': 2, 'video_codec': 2, 'hearing_impaired': 1}
|
||||
|
||||
#: Scores for movies
|
||||
movie_scores = {'hash': 119, 'title': 60, 'year': 30, 'release_group': 15,
|
||||
'format': 7, 'audio_codec': 3, 'resolution': 2, 'video_codec': 2, 'hearing_impaired': 1}
|
||||
|
||||
#: Equivalent release groups
|
||||
equivalent_release_groups = ({'LOL', 'DIMENSION'}, {'ASAP', 'IMMERSE', 'FLEET'})
|
||||
|
||||
|
||||
def get_episode_equations():
|
||||
"""Get the score equations for a :class:`~subliminal.video.Episode`
|
||||
def get_equivalent_release_groups(release_group):
|
||||
"""Get all the equivalents of the given release group.
|
||||
|
||||
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`
|
||||
:param str release_group: the release group to get the equivalents of.
|
||||
:return: the equivalent release groups.
|
||||
:rtype: set
|
||||
|
||||
"""
|
||||
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
|
||||
for equivalent_release_group in equivalent_release_groups:
|
||||
if release_group in equivalent_release_group:
|
||||
return equivalent_release_group
|
||||
|
||||
return {release_group}
|
||||
|
||||
|
||||
def get_movie_equations():
|
||||
"""Get the score equations for a :class:`~subliminal.video.Movie`
|
||||
def get_scores(video):
|
||||
"""Get the scores dict for the given `video`.
|
||||
|
||||
The equations are the following:
|
||||
This will return either :data:`episode_scores` or :data:`movie_scores` based on the type of the `video`.
|
||||
|
||||
1. hash = resolution + video_codec + audio_codec + title + year + release_group
|
||||
2. imdb_id = hash
|
||||
3. resolution = video_codec
|
||||
4. video_codec = 2 * audio_codec
|
||||
5. title = resolution + video_codec + audio_codec + year + 1
|
||||
6. release_group = resolution + video_codec + audio_codec + 1
|
||||
7. year = release_group + 1
|
||||
8. audio_codec = 1
|
||||
|
||||
:return: the score equations for a movie
|
||||
:rtype: list of :class:`sympy.Eq`
|
||||
:param video: the video to compute the score against.
|
||||
:type video: :class:`~subliminal.video.Video`
|
||||
:return: the scores dict.
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
equations = []
|
||||
equations.append(Eq(hash, resolution + video_codec + audio_codec + 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 isinstance(video, Episode):
|
||||
return episode_scores
|
||||
elif isinstance(video, Movie):
|
||||
return movie_scores
|
||||
|
||||
raise ValueError('video must be an instance of Episode or Movie')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(solve(get_episode_equations(), [release_group, resolution, video_codec, audio_codec, imdb_id,
|
||||
hash, series, tvdb_id, season, episode, title]))
|
||||
print(solve(get_movie_equations(), [release_group, resolution, video_codec, audio_codec, imdb_id,
|
||||
hash, title, year]))
|
||||
def compute_score(subtitle, video, hearing_impaired=None):
|
||||
"""Compute the score of the `subtitle` against the `video` with `hearing_impaired` preference.
|
||||
|
||||
:func:`compute_score` uses the :meth:`Subtitle.get_matches <subliminal.subtitle.Subtitle.get_matches>` method and
|
||||
applies the scores (either from :data:`episode_scores` or :data:`movie_scores`) after some processing.
|
||||
|
||||
:param subtitle: the subtitle to compute the score of.
|
||||
:type subtitle: :class:`~subliminal.subtitle.Subtitle`
|
||||
:param video: the video to compute the score against.
|
||||
:type video: :class:`~subliminal.video.Video`
|
||||
:param bool hearing_impaired: hearing impaired preference.
|
||||
:return: score of the subtitle.
|
||||
:rtype: int
|
||||
|
||||
"""
|
||||
logger.info('Computing score of %r for video %r with %r', subtitle, video, dict(hearing_impaired=hearing_impaired))
|
||||
|
||||
# get the scores dict
|
||||
scores = get_scores(video)
|
||||
logger.debug('Using scores %r', scores)
|
||||
|
||||
# get the matches
|
||||
matches = subtitle.get_matches(video)
|
||||
logger.debug('Found matches %r', matches)
|
||||
|
||||
# on hash match, discard everything else
|
||||
if 'hash' in matches:
|
||||
logger.debug('Keeping only hash match')
|
||||
matches &= {'hash'}
|
||||
|
||||
# handle equivalent matches
|
||||
if isinstance(video, Episode):
|
||||
if 'title' in matches:
|
||||
logger.debug('Adding title match equivalent')
|
||||
matches.add('episode')
|
||||
if 'series_imdb_id' in matches:
|
||||
logger.debug('Adding series_imdb_id match equivalent')
|
||||
matches |= {'series', 'year'}
|
||||
if 'imdb_id' in matches:
|
||||
logger.debug('Adding imdb_id match equivalents')
|
||||
matches |= {'series', 'year', 'season', 'episode'}
|
||||
if 'tvdb_id' in matches:
|
||||
logger.debug('Adding tvdb_id match equivalents')
|
||||
matches |= {'series', 'year', 'season', 'episode'}
|
||||
if 'series_tvdb_id' in matches:
|
||||
logger.debug('Adding series_tvdb_id match equivalents')
|
||||
matches |= {'series', 'year'}
|
||||
elif isinstance(video, Movie):
|
||||
if 'imdb_id' in matches:
|
||||
logger.debug('Adding imdb_id match equivalents')
|
||||
matches |= {'title', 'year'}
|
||||
|
||||
# handle hearing impaired
|
||||
if hearing_impaired is not None and subtitle.hearing_impaired == hearing_impaired:
|
||||
logger.debug('Matched hearing_impaired')
|
||||
matches.add('hearing_impaired')
|
||||
|
||||
# compute the score
|
||||
score = sum((scores.get(match, 0) for match in matches))
|
||||
logger.info('Computed score %r with final matches %r', score, matches)
|
||||
|
||||
# ensure score is within valid bounds
|
||||
assert 0 <= score <= scores['hash'] + scores['hearing_impaired']
|
||||
|
||||
return score
|
||||
|
||||
|
||||
def solve_episode_equations():
|
||||
from sympy import Eq, solve, symbols
|
||||
|
||||
hash, series, year, season, episode, release_group = symbols('hash series year season episode release_group')
|
||||
format, audio_codec, resolution, video_codec = symbols('format audio_codec resolution video_codec')
|
||||
hearing_impaired = symbols('hearing_impaired')
|
||||
|
||||
equations = [
|
||||
# hash is best
|
||||
Eq(hash, series + year + season + episode + release_group + format + audio_codec + resolution + video_codec),
|
||||
|
||||
# series counts for the most part in the total score
|
||||
Eq(series, year + season + episode + release_group + format + audio_codec + resolution + video_codec + 1),
|
||||
|
||||
# year is the second most important part
|
||||
Eq(year, season + episode + release_group + format + audio_codec + resolution + video_codec + 1),
|
||||
|
||||
# season is important too
|
||||
Eq(season, release_group + format + audio_codec + resolution + video_codec + 1),
|
||||
|
||||
# episode is equally important to season
|
||||
Eq(episode, season),
|
||||
|
||||
# release group is the next most wanted match
|
||||
Eq(release_group, format + audio_codec + resolution + video_codec + 1),
|
||||
|
||||
# format counts as much as audio_codec, resolution and video_codec
|
||||
Eq(format, audio_codec + resolution + video_codec),
|
||||
|
||||
# audio_codec is more valuable than video_codec
|
||||
Eq(audio_codec, video_codec + 1),
|
||||
|
||||
# resolution counts as much as video_codec
|
||||
Eq(resolution, video_codec),
|
||||
|
||||
# video_codec is the least valuable match but counts more than the sum of all scoring increasing matches
|
||||
Eq(video_codec, hearing_impaired + 1),
|
||||
|
||||
# hearing impaired is only used for score increasing, so put it to 1
|
||||
Eq(hearing_impaired, 1),
|
||||
]
|
||||
|
||||
return solve(equations, [hash, series, year, season, episode, release_group, format, audio_codec, resolution,
|
||||
hearing_impaired, video_codec])
|
||||
|
||||
|
||||
def solve_movie_equations():
|
||||
from sympy import Eq, solve, symbols
|
||||
|
||||
hash, title, year, release_group = symbols('hash title year release_group')
|
||||
format, audio_codec, resolution, video_codec = symbols('format audio_codec resolution video_codec')
|
||||
hearing_impaired = symbols('hearing_impaired')
|
||||
|
||||
equations = [
|
||||
# hash is best
|
||||
Eq(hash, title + year + release_group + format + audio_codec + resolution + video_codec),
|
||||
|
||||
# title counts for the most part in the total score
|
||||
Eq(title, year + release_group + format + audio_codec + resolution + video_codec + 1),
|
||||
|
||||
# year is the second most important part
|
||||
Eq(year, release_group + format + audio_codec + resolution + video_codec + 1),
|
||||
|
||||
# release group is the next most wanted match
|
||||
Eq(release_group, format + audio_codec + resolution + video_codec + 1),
|
||||
|
||||
# format counts as much as audio_codec, resolution and video_codec
|
||||
Eq(format, audio_codec + resolution + video_codec),
|
||||
|
||||
# audio_codec is more valuable than video_codec
|
||||
Eq(audio_codec, video_codec + 1),
|
||||
|
||||
# resolution counts as much as video_codec
|
||||
Eq(resolution, video_codec),
|
||||
|
||||
# video_codec is the least valuable match but counts more than the sum of all scoring increasing matches
|
||||
Eq(video_codec, hearing_impaired + 1),
|
||||
|
||||
# hearing impaired is only used for score increasing, so put it to 1
|
||||
Eq(hearing_impaired, 1),
|
||||
]
|
||||
|
||||
return solve(equations, [hash, title, year, release_group, format, audio_codec, resolution, hearing_impaired,
|
||||
video_codec])
|
||||
|
||||
+205
-107
@@ -1,157 +1,255 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import codecs
|
||||
import logging
|
||||
import os.path
|
||||
import babelfish
|
||||
import os
|
||||
|
||||
import chardet
|
||||
import pysrt
|
||||
|
||||
from .score import get_equivalent_release_groups
|
||||
from .video import Episode, Movie
|
||||
from .utils import sanitize, sanitize_release_group
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#: Subtitle extensions
|
||||
SUBTITLE_EXTENSIONS = ('.srt', '.sub', '.smi', '.txt', '.ssa', '.ass', '.mpl')
|
||||
|
||||
|
||||
class Subtitle(object):
|
||||
"""Base class for subtitle
|
||||
"""Base class for subtitle.
|
||||
|
||||
:param language: language of the subtitle
|
||||
:type language: :class:`babelfish.Language`
|
||||
:param bool hearing_impaired: `True` if the subtitle is hearing impaired, `False` otherwise
|
||||
:param language: language of the subtitle.
|
||||
:type language: :class:`~babelfish.language.Language`
|
||||
:param bool hearing_impaired: whether or not the subtitle is hearing impaired.
|
||||
:param page_link: URL of the web page from which the subtitle can be downloaded.
|
||||
:type page_link: str
|
||||
:param encoding: Text encoding of the subtitle.
|
||||
:type encoding: str
|
||||
|
||||
"""
|
||||
def __init__(self, language, hearing_impaired=False):
|
||||
#: Name of the provider that returns that class of subtitle
|
||||
provider_name = ''
|
||||
|
||||
def __init__(self, language, hearing_impaired=False, page_link=None, encoding=None):
|
||||
#: Language of the subtitle
|
||||
self.language = language
|
||||
|
||||
#: Whether or not the subtitle is hearing impaired
|
||||
self.hearing_impaired = hearing_impaired
|
||||
|
||||
def compute_matches(self, video):
|
||||
"""Compute the matches of the subtitle against the `video`
|
||||
#: URL of the web page from which the subtitle can be downloaded
|
||||
self.page_link = page_link
|
||||
|
||||
:param video: the video to compute the matches against
|
||||
#: Content as bytes
|
||||
self.content = None
|
||||
|
||||
#: Encoding to decode with when accessing :attr:`text`
|
||||
self.encoding = None
|
||||
|
||||
# validate the encoding
|
||||
if encoding:
|
||||
try:
|
||||
self.encoding = codecs.lookup(encoding).name
|
||||
except (TypeError, LookupError):
|
||||
logger.debug('Unsupported encoding %s', encoding)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Unique identifier of the subtitle"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
"""Content as string
|
||||
|
||||
If :attr:`encoding` is None, the encoding is guessed with :meth:`guess_encoding`
|
||||
|
||||
"""
|
||||
if not self.content:
|
||||
return
|
||||
|
||||
if self.encoding:
|
||||
return self.content.decode(self.encoding, errors='replace')
|
||||
|
||||
return self.content.decode(self.guess_encoding(), errors='replace')
|
||||
|
||||
def is_valid(self):
|
||||
"""Check if a :attr:`text` is a valid SubRip format.
|
||||
|
||||
:return: whether or not the subtitle is valid.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if not self.text:
|
||||
return False
|
||||
|
||||
try:
|
||||
pysrt.from_string(self.text, error_handling=pysrt.ERROR_RAISE)
|
||||
except pysrt.Error as e:
|
||||
if e.args[0] < 80:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def guess_encoding(self):
|
||||
"""Guess encoding using the language, falling back on chardet.
|
||||
|
||||
:return: the guessed encoding.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
logger.info('Guessing encoding for language %s', self.language)
|
||||
|
||||
# always try utf-8 first
|
||||
encodings = ['utf-8']
|
||||
|
||||
# add language-specific encodings
|
||||
if self.language.alpha3 == 'zho':
|
||||
encodings.extend(['gb18030', 'big5'])
|
||||
elif self.language.alpha3 == 'jpn':
|
||||
encodings.append('shift-jis')
|
||||
elif self.language.alpha3 == 'ara':
|
||||
encodings.append('windows-1256')
|
||||
elif self.language.alpha3 == 'heb':
|
||||
encodings.append('windows-1255')
|
||||
elif self.language.alpha3 == 'tur':
|
||||
encodings.extend(['iso-8859-9', 'windows-1254'])
|
||||
elif self.language.alpha3 == 'pol':
|
||||
# Eastern European Group 1
|
||||
encodings.extend(['windows-1250'])
|
||||
elif self.language.alpha3 == 'bul':
|
||||
# Eastern European Group 2
|
||||
encodings.extend(['windows-1251'])
|
||||
else:
|
||||
# Western European (windows-1252)
|
||||
encodings.append('latin-1')
|
||||
|
||||
# try to decode
|
||||
logger.debug('Trying encodings %r', encodings)
|
||||
for encoding in encodings:
|
||||
try:
|
||||
self.content.decode(encoding)
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
else:
|
||||
logger.info('Guessed encoding %s', encoding)
|
||||
return encoding
|
||||
|
||||
logger.warning('Could not guess encoding from language')
|
||||
|
||||
# fallback on chardet
|
||||
encoding = chardet.detect(self.content)['encoding']
|
||||
logger.info('Chardet found encoding %s', encoding)
|
||||
|
||||
return encoding
|
||||
|
||||
def get_matches(self, video):
|
||||
"""Get the matches against the `video`.
|
||||
|
||||
:param video: the video to get the matches with.
|
||||
:type video: :class:`~subliminal.video.Video`
|
||||
:return: matches of the subtitle
|
||||
:return: matches of the subtitle.
|
||||
:rtype: set
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def compute_score(self, video):
|
||||
"""Compute the score of the subtitle against the `video`
|
||||
|
||||
There are equivalent matches so that a provider can match one element or its equivalent. This is
|
||||
to give all provider a chance to have a score in the same range without hurting quality.
|
||||
|
||||
* Matching :class:`~subliminal.video.Video`'s `hashes` is equivalent to matching everything else
|
||||
* Matching :class:`~subliminal.video.Episode`'s `season` and `episode`
|
||||
is equivalent to matching :class:`~subliminal.video.Episode`'s `title`
|
||||
* Matching :class:`~subliminal.video.Episode`'s `tvdb_id` is equivalent to matching
|
||||
:class:`~subliminal.video.Episode`'s `series`
|
||||
|
||||
:param video: the video to compute the score against
|
||||
:type video: :class:`~subliminal.video.Video`
|
||||
:return: score of the subtitle
|
||||
:rtype: int
|
||||
|
||||
"""
|
||||
score = 0
|
||||
# compute matches
|
||||
initial_matches = self.compute_matches(video)
|
||||
matches = initial_matches.copy()
|
||||
# hash is the perfect match
|
||||
if 'hash' in matches:
|
||||
score = video.scores['hash']
|
||||
else:
|
||||
# remove equivalences
|
||||
if isinstance(video, Episode):
|
||||
if 'imdb_id' in matches:
|
||||
matches -= {'series', 'tvdb_id', 'season', 'episode', 'title'}
|
||||
if 'tvdb_id' in matches:
|
||||
matches -= {'series'}
|
||||
if 'title' in matches:
|
||||
matches -= {'season', 'episode'}
|
||||
# add other scores
|
||||
score += sum((video.scores[match] for match in matches))
|
||||
logger.info('Computed score %d with matches %r', score, initial_matches)
|
||||
return score
|
||||
def __hash__(self):
|
||||
return hash(self.provider_name + '-' + self.id)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s [%s]>' % (self.__class__.__name__, self.language)
|
||||
return '<%s %r [%s]>' % (self.__class__.__name__, self.id, self.language)
|
||||
|
||||
|
||||
def get_subtitle_path(video_path, language=None):
|
||||
"""Create the subtitle path from the given `video_path` and `language`
|
||||
def get_subtitle_path(video_path, language=None, extension='.srt'):
|
||||
"""Get the subtitle path using the `video_path` and `language`.
|
||||
|
||||
:param string video_path: path to the video
|
||||
:param language: language of the subtitle to put in the path
|
||||
:type language: :class:`babelfish.Language` or None
|
||||
:return: path of the subtitle
|
||||
:rtype: string
|
||||
:param str video_path: path to the video.
|
||||
:param language: language of the subtitle to put in the path.
|
||||
:type language: :class:`~babelfish.language.Language`
|
||||
:param str extension: extension of the subtitle.
|
||||
:return: path of the subtitle.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
subtitle_path = os.path.splitext(video_path)[0]
|
||||
if language is not None:
|
||||
try:
|
||||
return subtitle_path + '.%s.%s' % (language.alpha2, 'srt')
|
||||
except babelfish.LanguageConvertError:
|
||||
return subtitle_path + '.%s.%s' % (language.alpha3, 'srt')
|
||||
return subtitle_path + '.srt'
|
||||
subtitle_root = os.path.splitext(video_path)[0]
|
||||
|
||||
if language:
|
||||
subtitle_root += '.' + str(language)
|
||||
|
||||
return subtitle_root + extension
|
||||
|
||||
|
||||
def is_valid_subtitle(subtitle_text):
|
||||
"""Check if a subtitle text is a valid SubRip format
|
||||
def guess_matches(video, guess, partial=False):
|
||||
"""Get matches between a `video` and a `guess`.
|
||||
|
||||
:return: `True` if the subtitle is valid, `False` otherwise
|
||||
:rtype: bool
|
||||
If a guess is `partial`, the absence information won't be counted as a match.
|
||||
|
||||
"""
|
||||
try:
|
||||
pysrt.from_string(subtitle_text, error_handling=pysrt.ERROR_RAISE)
|
||||
return True
|
||||
except pysrt.Error as e:
|
||||
if e.args[0] > 80:
|
||||
return True
|
||||
except:
|
||||
logger.exception('Unexpected error when validating subtitle')
|
||||
return False
|
||||
|
||||
|
||||
def compute_guess_matches(video, guess):
|
||||
"""Compute matches between a `video` and a `guess`
|
||||
|
||||
:param video: the video to compute the matches on
|
||||
:param video: the video.
|
||||
:type video: :class:`~subliminal.video.Video`
|
||||
:param guess: the guess to compute the matches on
|
||||
:type guess: :class:`guessit.Guess`
|
||||
:return: matches of the `guess`
|
||||
:param guess: the guess.
|
||||
:type guess: dict
|
||||
:param bool partial: whether or not the guess is partial.
|
||||
:return: matches between the `video` and the `guess`.
|
||||
:rtype: set
|
||||
|
||||
"""
|
||||
matches = set()
|
||||
if isinstance(video, Episode):
|
||||
# Series
|
||||
if video.series and 'series' in guess and guess['series'].lower() == video.series.lower():
|
||||
# series
|
||||
if video.series and 'title' in guess and sanitize(guess['title']) == sanitize(video.series):
|
||||
matches.add('series')
|
||||
# Season
|
||||
if video.season and 'seasonNumber' in guess and guess['seasonNumber'] == video.season:
|
||||
# title
|
||||
if video.title and 'episode_title' in guess and sanitize(guess['episode_title']) == sanitize(video.title):
|
||||
matches.add('title')
|
||||
# season
|
||||
if video.season and 'season' in guess and guess['season'] == video.season:
|
||||
matches.add('season')
|
||||
# Episode
|
||||
if video.episode and 'episodeNumber' in guess and guess['episodeNumber'] == video.episode:
|
||||
# episode
|
||||
if video.episode and 'episode' in guess and guess['episode'] == video.episode:
|
||||
matches.add('episode')
|
||||
elif isinstance(video, Movie):
|
||||
# Year
|
||||
# year
|
||||
if video.year and 'year' in guess and guess['year'] == video.year:
|
||||
matches.add('year')
|
||||
# Title
|
||||
if video.title and 'title' in guess and guess['title'].lower() == video.title.lower():
|
||||
matches.add('title')
|
||||
# Release group
|
||||
if video.release_group and 'releaseGroup' in guess and guess['releaseGroup'].lower() == video.release_group.lower():
|
||||
# count "no year" as an information
|
||||
if not partial and video.original_series and 'year' not in guess:
|
||||
matches.add('year')
|
||||
elif isinstance(video, Movie):
|
||||
# year
|
||||
if video.year and 'year' in guess and guess['year'] == video.year:
|
||||
matches.add('year')
|
||||
# title
|
||||
if video.title and 'title' in guess and sanitize(guess['title']) == sanitize(video.title):
|
||||
matches.add('title')
|
||||
# release_group
|
||||
if (video.release_group and 'release_group' in guess and
|
||||
sanitize_release_group(guess['release_group']) in
|
||||
get_equivalent_release_groups(sanitize_release_group(video.release_group))):
|
||||
matches.add('release_group')
|
||||
# Screen size
|
||||
if video.resolution and 'screenSize' in guess and guess['screenSize'] == video.resolution:
|
||||
# resolution
|
||||
if video.resolution and 'screen_size' in guess and guess['screen_size'] == video.resolution:
|
||||
matches.add('resolution')
|
||||
# Video codec
|
||||
if video.video_codec and 'videoCodec' in guess and guess['videoCodec'] == video.video_codec:
|
||||
# format
|
||||
if video.format and 'format' in guess and guess['format'].lower() == video.format.lower():
|
||||
matches.add('format')
|
||||
# video_codec
|
||||
if video.video_codec and 'video_codec' in guess and guess['video_codec'] == video.video_codec:
|
||||
matches.add('video_codec')
|
||||
# Audio codec
|
||||
if video.audio_codec and 'audioCodec' in guess and guess['audioCodec'] == video.audio_codec:
|
||||
# audio_codec
|
||||
if video.audio_codec and 'audio_codec' in guess and guess['audio_codec'] == video.audio_codec:
|
||||
matches.add('audio_codec')
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def fix_line_ending(content):
|
||||
"""Fix line ending of `content` by changing it to \n.
|
||||
|
||||
:param bytes content: content of the subtitle.
|
||||
:return: the content with fixed line endings.
|
||||
:rtype: bytes
|
||||
|
||||
"""
|
||||
return content.replace(b'\r\n', b'\n').replace(b'\r', b'\n')
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
from unittest import TextTestRunner, TestSuite
|
||||
from subliminal import cache_region
|
||||
from . import test_providers, test_subliminal
|
||||
|
||||
|
||||
cache_region.configure('dogpile.cache.memory', expiration_time=60 * 30) # @UndefinedVariable
|
||||
suite = TestSuite([test_providers.suite(), test_subliminal.suite()])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
TextTestRunner().run(suite)
|
||||
@@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
from subliminal import Movie, Episode
|
||||
|
||||
|
||||
MOVIES = [Movie('Man of Steel (2013)/man.of.steel.2013.720p.bluray.x264-felony.mkv', 'Man of Steel',
|
||||
release_group='felony', resolution='720p', video_codec='h264', audio_codec='DTS', imdb_id=770828,
|
||||
size=7033732714, year=2013,
|
||||
hashes={'opensubtitles': '5b8f8f4e41ccb21e', 'thesubdb': 'ad32876133355929d814457537e12dc2'})]
|
||||
|
||||
EPISODES = [Episode('The Big Bang Theory/Season 07/The.Big.Bang.Theory.S07E05.720p.HDTV.X264-DIMENSION.mkv',
|
||||
'The Big Bang Theory', 7, 5, release_group='DIMENSION', resolution='720p', video_codec='h264',
|
||||
audio_codec='AC3', imdb_id=3229392, size=501910737, title='The Workplace Proximity',
|
||||
tvdb_id=80379,
|
||||
hashes={'opensubtitles': '6878b3ef7c1bd19e', 'thesubdb': '9dbbfb7ba81c9a6237237dae8589fccc'}),
|
||||
Episode('Game of Thrones/Season 03/Game.of.Thrones.S03E10.Mhysa.720p.WEB-DL.DD5.1.H.264-NTb.mkv',
|
||||
'Game of Thrones', 3, 10, release_group='NTb', resolution='720p', video_codec='h264',
|
||||
audio_codec='AC3', imdb_id=2178796, size=2142810931, title='Mhysa', tvdb_id=121361,
|
||||
hashes={'opensubtitles': 'b850baa096976c22', 'thesubdb': 'b1f899c77f4c960b84b8dbf840d4e42d'})]
|
||||
@@ -1,453 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import os
|
||||
from unittest import TestCase, TestSuite, TestLoader, TextTestRunner
|
||||
from babelfish import Language
|
||||
from pkg_resources import iter_entry_points
|
||||
from subliminal import PROVIDERS_ENTRY_POINT
|
||||
from subliminal.subtitle import is_valid_subtitle
|
||||
from subliminal.tests.common import MOVIES, EPISODES
|
||||
|
||||
|
||||
class ProviderTestCase(TestCase):
|
||||
provider_name = ''
|
||||
|
||||
def setUp(self):
|
||||
for provider_entry_point in iter_entry_points(PROVIDERS_ENTRY_POINT, self.provider_name):
|
||||
self.Provider = provider_entry_point.load()
|
||||
break
|
||||
|
||||
|
||||
class Addic7edProviderTestCase(ProviderTestCase):
|
||||
provider_name = 'addic7ed'
|
||||
|
||||
def test_find_show_id(self):
|
||||
with self.Provider() as provider:
|
||||
show_id = provider.find_show_id('The Big Bang')
|
||||
self.assertEqual(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.assertIsNone(show_id)
|
||||
|
||||
def test_get_show_ids(self):
|
||||
with self.Provider() as provider:
|
||||
show_ids = provider.get_show_ids()
|
||||
self.assertIn('the big bang theory', show_ids)
|
||||
self.assertEqual(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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual(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.assertIsNone(show_id)
|
||||
|
||||
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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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([]),
|
||||
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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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([]),
|
||||
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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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']),
|
||||
frozenset(['video_codec', 'title', 'resolution', 'audio_codec', 'year'])}
|
||||
with self.Provider() as provider:
|
||||
subtitles = provider.query(language, title=video.title, year=video.year)
|
||||
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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']),
|
||||
frozenset(['video_codec', 'title', 'resolution', 'audio_codec', 'year'])}
|
||||
with self.Provider() as provider:
|
||||
subtitles = provider.list_subtitles(video, languages)
|
||||
self.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual(show_id, 154)
|
||||
|
||||
def test_find_show_id_ambiguous(self):
|
||||
with self.Provider() as provider:
|
||||
show_id = provider.find_show_id('New Girl')
|
||||
self.assertEqual(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.assertEqual(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.assertIsNone(show_id)
|
||||
|
||||
def test_find_episode_ids(self):
|
||||
with self.Provider() as provider:
|
||||
episode_ids = provider.find_episode_ids(154, 5)
|
||||
self.assertEqual(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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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.assertEqual({frozenset(subtitle.compute_matches(video)) for subtitle in subtitles}, matches)
|
||||
self.assertEqual({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())
|
||||
@@ -1,174 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import os
|
||||
import shutil
|
||||
from unittest import TestCase, TestSuite, TestLoader, TextTestRunner
|
||||
from babelfish import Language
|
||||
from subliminal import list_subtitles, download_subtitles, download_best_subtitles, scan_video
|
||||
from subliminal.tests.common import MOVIES, EPISODES
|
||||
|
||||
|
||||
TEST_DIR = 'test_data'
|
||||
|
||||
|
||||
class ApiTestCase(TestCase):
|
||||
def setUp(self):
|
||||
os.mkdir(TEST_DIR)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(TEST_DIR)
|
||||
|
||||
def test_list_subtitles_movie_0(self):
|
||||
videos = [MOVIES[0]]
|
||||
languages = {Language('eng')}
|
||||
subtitles = list_subtitles(videos, languages)
|
||||
self.assertEqual(len(subtitles), len(videos))
|
||||
self.assertGreater(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.assertEqual(len(subtitles), len(videos))
|
||||
self.assertGreater(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.assertEqual(len(subtitles), len(videos))
|
||||
self.assertGreater(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.assertEqual(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.assertIn(video, subtitles)
|
||||
self.assertEqual(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.assertEqual(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)
|
||||
|
||||
|
||||
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.assertEqual(scanned_video.name, os.path.join(TEST_DIR, os.path.split(video.name)[1]))
|
||||
self.assertEqual(scanned_video.title.lower(), video.title.lower())
|
||||
self.assertEqual(scanned_video.year, video.year)
|
||||
self.assertEqual(scanned_video.video_codec, video.video_codec)
|
||||
self.assertEqual(scanned_video.resolution, video.resolution)
|
||||
self.assertEqual(scanned_video.release_group, video.release_group)
|
||||
self.assertEqual(scanned_video.subtitle_languages, set())
|
||||
self.assertEqual(scanned_video.hashes, {})
|
||||
self.assertIsNone(scanned_video.audio_codec)
|
||||
self.assertIsNone(scanned_video.imdb_id)
|
||||
self.assertEqual(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.assertEqual(scanned_video.name, os.path.join(TEST_DIR, os.path.split(video.name)[1]))
|
||||
self.assertEqual(scanned_video.series, video.series)
|
||||
self.assertEqual(scanned_video.season, video.season)
|
||||
self.assertEqual(scanned_video.episode, video.episode)
|
||||
self.assertEqual(scanned_video.video_codec, video.video_codec)
|
||||
self.assertEqual(scanned_video.resolution, video.resolution)
|
||||
self.assertEqual(scanned_video.release_group, video.release_group)
|
||||
self.assertEqual(scanned_video.subtitle_languages, set())
|
||||
self.assertEqual(scanned_video.hashes, {})
|
||||
self.assertIsNone(scanned_video.title)
|
||||
self.assertIsNone(scanned_video.tvdb_id)
|
||||
self.assertIsNone(scanned_video.imdb_id)
|
||||
self.assertIsNone(scanned_video.audio_codec)
|
||||
self.assertEqual(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.assertEqual(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.assertEqual(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.assertEqual(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())
|
||||
@@ -0,0 +1,152 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
|
||||
|
||||
def hash_opensubtitles(video_path):
|
||||
"""Compute a hash using OpenSubtitles' algorithm.
|
||||
|
||||
:param str video_path: path of the video.
|
||||
:return: the hash.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
bytesize = struct.calcsize(b'<q')
|
||||
with open(video_path, 'rb') as f:
|
||||
filesize = os.path.getsize(video_path)
|
||||
filehash = filesize
|
||||
if filesize < 65536 * 2:
|
||||
return
|
||||
for _ in range(65536 // bytesize):
|
||||
filebuffer = f.read(bytesize)
|
||||
(l_value,) = struct.unpack(b'<q', filebuffer)
|
||||
filehash += l_value
|
||||
filehash &= 0xFFFFFFFFFFFFFFFF # to remain as 64bit number
|
||||
f.seek(max(0, filesize - 65536), 0)
|
||||
for _ in range(65536 // bytesize):
|
||||
filebuffer = f.read(bytesize)
|
||||
(l_value,) = struct.unpack(b'<q', filebuffer)
|
||||
filehash += l_value
|
||||
filehash &= 0xFFFFFFFFFFFFFFFF
|
||||
returnedhash = '%016x' % filehash
|
||||
|
||||
return returnedhash
|
||||
|
||||
|
||||
def hash_thesubdb(video_path):
|
||||
"""Compute a hash using TheSubDB's algorithm.
|
||||
|
||||
:param str video_path: path of the video.
|
||||
:return: the hash.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
readsize = 64 * 1024
|
||||
if os.path.getsize(video_path) < readsize:
|
||||
return
|
||||
with open(video_path, 'rb') as f:
|
||||
data = f.read(readsize)
|
||||
f.seek(-readsize, os.SEEK_END)
|
||||
data += f.read(readsize)
|
||||
|
||||
return hashlib.md5(data).hexdigest()
|
||||
|
||||
|
||||
def hash_napiprojekt(video_path):
|
||||
"""Compute a hash using NapiProjekt's algorithm.
|
||||
|
||||
:param str video_path: path of the video.
|
||||
:return: the hash.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
readsize = 1024 * 1024 * 10
|
||||
with open(video_path, 'rb') as f:
|
||||
data = f.read(readsize)
|
||||
return hashlib.md5(data).hexdigest()
|
||||
|
||||
|
||||
def hash_shooter(video_path):
|
||||
"""Compute a hash using Shooter's algorithm
|
||||
|
||||
:param string video_path: path of the video
|
||||
:return: the hash
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
filesize = os.path.getsize(video_path)
|
||||
readsize = 4096
|
||||
if os.path.getsize(video_path) < readsize * 2:
|
||||
return None
|
||||
offsets = (readsize, filesize // 3 * 2, filesize // 3, filesize - readsize * 2)
|
||||
filehash = []
|
||||
with open(video_path, 'rb') as f:
|
||||
for offset in offsets:
|
||||
f.seek(offset)
|
||||
filehash.append(hashlib.md5(f.read(readsize)).hexdigest())
|
||||
return ';'.join(filehash)
|
||||
|
||||
|
||||
def sanitize(string, ignore_characters=None):
|
||||
"""Sanitize a string to strip special characters.
|
||||
|
||||
:param str string: the string to sanitize.
|
||||
:param set ignore_characters: characters to ignore.
|
||||
:return: the sanitized string.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
# only deal with strings
|
||||
if string is None:
|
||||
return
|
||||
|
||||
ignore_characters = ignore_characters or set()
|
||||
|
||||
# replace some characters with one space
|
||||
characters = {'-', ':', '(', ')', '.'} - ignore_characters
|
||||
if characters:
|
||||
string = re.sub(r'[%s]' % re.escape(''.join(characters)), ' ', string)
|
||||
|
||||
# remove some characters
|
||||
characters = {'\''} - ignore_characters
|
||||
if characters:
|
||||
string = re.sub(r'[%s]' % re.escape(''.join(characters)), '', string)
|
||||
|
||||
# replace multiple spaces with one
|
||||
string = re.sub(r'\s+', ' ', string)
|
||||
|
||||
# strip and lower case
|
||||
return string.strip().lower()
|
||||
|
||||
|
||||
def sanitize_release_group(string):
|
||||
"""Sanitize a `release_group` string to remove content in square brackets.
|
||||
|
||||
:param str string: the release group to sanitize.
|
||||
:return: the sanitized release group.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
# only deal with strings
|
||||
if string is None:
|
||||
return
|
||||
|
||||
# remove content in square brackets
|
||||
string = re.sub(r'\[\w+\]', '', string)
|
||||
|
||||
# strip and upper case
|
||||
return string.strip().upper()
|
||||
|
||||
|
||||
def timestamp(date):
|
||||
"""Get the timestamp of the `date`, python2/3 compatible
|
||||
|
||||
:param datetime.datetime date: the utc date.
|
||||
:return: the timestamp of the date.
|
||||
:rtype: float
|
||||
|
||||
"""
|
||||
return (date - datetime(1970, 1, 1)).total_seconds()
|
||||
+138
-298
@@ -1,14 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import datetime
|
||||
import hashlib
|
||||
from __future__ import division
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
import babelfish
|
||||
import enzyme
|
||||
import guessit
|
||||
|
||||
from guessit import guessit
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,54 +13,99 @@ VIDEO_EXTENSIONS = ('.3g2', '.3gp', '.3gp2', '.3gpp', '.60d', '.ajp', '.asf', '.
|
||||
'.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')
|
||||
'.mpe', '.mpeg', '.mpg', '.mpv', '.mpv2', '.mxf', '.nsv', '.nut', '.ogg', '.ogm' '.ogv', '.omf',
|
||||
'.ps', '.qt', '.ram', '.rm', '.rmvb', '.swf', '.ts', '.vfw', '.vid', '.video', '.viv', '.vivo',
|
||||
'.vob', '.vro', '.wm', '.wmv', '.wmx', '.wrap', '.wvx', '.wx', '.x264', '.xvid')
|
||||
|
||||
|
||||
class Video(object):
|
||||
"""Base class for videos
|
||||
"""Base class for videos.
|
||||
|
||||
Represent a video, existing or not, with various properties that defines it.
|
||||
Each property has an associated score based on equations that are described in
|
||||
subclasses.
|
||||
Represent a video, existing or not.
|
||||
|
||||
:param string name: name or path of the video
|
||||
:param string release_group: release group of the video
|
||||
:param string resolution: screen size of the video stream (480p, 720p, 1080p or 1080i)
|
||||
:param string video_codec: codec of the video stream
|
||||
:param string audio_codec: codec of the main audio stream
|
||||
:param int imdb_id: IMDb id of the video
|
||||
:param dict hashes: hashes of the video file by provider names
|
||||
:param int size: byte size of the video file
|
||||
:param set subtitle_languages: existing subtitle languages
|
||||
:param str name: name or path of the video.
|
||||
:param str format: format of the video (HDTV, WEB-DL, BluRay, ...).
|
||||
:param str release_group: release group of the video.
|
||||
:param str resolution: resolution of the video stream (480p, 720p, 1080p or 1080i).
|
||||
:param str video_codec: codec of the video stream.
|
||||
:param str audio_codec: codec of the main audio stream.
|
||||
:param str imdb_id: IMDb id of the video.
|
||||
:param dict hashes: hashes of the video file by provider names.
|
||||
:param int size: size of the video file in bytes.
|
||||
:param set subtitle_languages: existing subtitle languages.
|
||||
|
||||
"""
|
||||
scores = {}
|
||||
|
||||
def __init__(self, name, release_group=None, resolution=None, video_codec=None, audio_codec=None, imdb_id=None,
|
||||
hashes=None, size=None, subtitle_languages=None):
|
||||
def __init__(self, name, format=None, release_group=None, resolution=None, video_codec=None, audio_codec=None,
|
||||
imdb_id=None, hashes=None, size=None, subtitle_languages=None):
|
||||
#: Name or path of the video
|
||||
self.name = name
|
||||
|
||||
#: Format of the video (HDTV, WEB-DL, BluRay, ...)
|
||||
self.format = format
|
||||
|
||||
#: Release group of the video
|
||||
self.release_group = release_group
|
||||
|
||||
#: Resolution of the video stream (480p, 720p, 1080p or 1080i)
|
||||
self.resolution = resolution
|
||||
|
||||
#: Codec of the video stream
|
||||
self.video_codec = video_codec
|
||||
|
||||
#: Codec of the main audio stream
|
||||
self.audio_codec = audio_codec
|
||||
|
||||
#: IMDb id of the video
|
||||
self.imdb_id = imdb_id
|
||||
|
||||
#: Hashes of the video file by provider names
|
||||
self.hashes = hashes or {}
|
||||
|
||||
#: Size of the video file in bytes
|
||||
self.size = size
|
||||
|
||||
#: Existing subtitle languages
|
||||
self.subtitle_languages = subtitle_languages or set()
|
||||
|
||||
@property
|
||||
def exists(self):
|
||||
"""Test whether the video exists"""
|
||||
return os.path.exists(self.name)
|
||||
|
||||
@property
|
||||
def age(self):
|
||||
"""Age of the video"""
|
||||
if self.exists:
|
||||
return datetime.utcnow() - datetime.utcfromtimestamp(os.path.getmtime(self.name))
|
||||
|
||||
return timedelta()
|
||||
|
||||
@classmethod
|
||||
def fromguess(cls, name, guess):
|
||||
"""Create an :class:`Episode` or a :class:`Movie` with the given `name` based on the `guess`.
|
||||
|
||||
:param str name: name of the video.
|
||||
:param dict guess: guessed data.
|
||||
:raise: :class:`ValueError` if the `type` of the `guess` is invalid
|
||||
|
||||
"""
|
||||
if guess['type'] == 'episode':
|
||||
return Episode.fromguess(name, guess)
|
||||
|
||||
if guess['type'] == 'movie':
|
||||
return Movie.fromguess(name, guess)
|
||||
|
||||
raise ValueError('The guess must be an episode or a movie guess')
|
||||
|
||||
@classmethod
|
||||
def fromname(cls, name):
|
||||
"""Shortcut for :meth:`fromguess` with a `guess` guessed from the `name`.
|
||||
|
||||
:param str name: name of the video.
|
||||
|
||||
"""
|
||||
return cls.fromguess(name, guessit(name))
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s [%r]>' % (self.__class__.__name__, self.name)
|
||||
|
||||
@@ -73,309 +114,108 @@ class Video(object):
|
||||
|
||||
|
||||
class Episode(Video):
|
||||
"""Episode :class:`Video`
|
||||
"""Episode :class:`Video`.
|
||||
|
||||
Scores are defined by a set of equations, see :func:`~subliminal.score.get_episode_equations`
|
||||
|
||||
:param string series: series of the episode
|
||||
:param int season: season number of the episode
|
||||
:param int episode: episode number of the episode
|
||||
:param string title: title of the episode
|
||||
:param int tvdb_id: TheTVDB id of the episode
|
||||
:param str series: series of the episode.
|
||||
:param int season: season number of the episode.
|
||||
:param int episode: episode number of the episode.
|
||||
:param str title: title of the episode.
|
||||
:param int year: year of the series.
|
||||
:param bool original_series: whether the series is the first with this name.
|
||||
:param int tvdb_id: TVDB id of the episode.
|
||||
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
|
||||
|
||||
"""
|
||||
scores = {'title': 12, 'video_codec': 2, 'imdb_id': 35, 'audio_codec': 1, 'tvdb_id': 23, 'resolution': 2,
|
||||
'season': 6, 'release_group': 6, 'series': 23, 'episode': 6, 'hash': 46}
|
||||
def __init__(self, name, series, season, episode, title=None, year=None, original_series=True, tvdb_id=None,
|
||||
series_tvdb_id=None, series_imdb_id=None, **kwargs):
|
||||
super(Episode, self).__init__(name, **kwargs)
|
||||
|
||||
def __init__(self, name, series, season, episode, release_group=None, resolution=None, video_codec=None,
|
||||
audio_codec=None, imdb_id=None, hashes=None, size=None, subtitle_languages=None, title=None,
|
||||
tvdb_id=None):
|
||||
super(Episode, self).__init__(name, release_group, resolution, video_codec, audio_codec, imdb_id, hashes,
|
||||
size, subtitle_languages)
|
||||
#: Series of the episode
|
||||
self.series = series
|
||||
|
||||
#: Season number of the episode
|
||||
self.season = season
|
||||
|
||||
#: Episode number of the episode
|
||||
self.episode = episode
|
||||
|
||||
#: Title of the episode
|
||||
self.title = title
|
||||
|
||||
#: Year of series
|
||||
self.year = year
|
||||
|
||||
#: The series is the first with this name
|
||||
self.original_series = original_series
|
||||
|
||||
#: TVDB id of the episode
|
||||
self.tvdb_id = tvdb_id
|
||||
|
||||
#: TVDB id of the series
|
||||
self.series_tvdb_id = series_tvdb_id
|
||||
|
||||
#: IMDb id of the series
|
||||
self.series_imdb_id = series_imdb_id
|
||||
|
||||
@classmethod
|
||||
def fromguess(cls, name, guess):
|
||||
if guess['type'] != 'episode':
|
||||
raise ValueError('The guess must be an episode guess')
|
||||
if 'series' not in guess or 'season' not in guess or 'episodeNumber' not in guess:
|
||||
|
||||
if 'title' not in guess or 'episode' not in guess:
|
||||
raise ValueError('Insufficient data to process the guess')
|
||||
return cls(name, guess['series'], guess['season'], guess['episodeNumber'],
|
||||
release_group=guess.get('releaseGroup'), resolution=guess.get('screenSize'),
|
||||
video_codec=guess.get('videoCodec'), audio_codec=guess.get('audioCodec'),
|
||||
title=guess.get('title'))
|
||||
|
||||
return cls(name, guess['title'], guess.get('season', 1), guess['episode'], title=guess.get('episode_title'),
|
||||
year=guess.get('year'), format=guess.get('format'), original_series='year' not in guess,
|
||||
release_group=guess.get('release_group'), resolution=guess.get('screen_size'),
|
||||
video_codec=guess.get('video_codec'), audio_codec=guess.get('audio_codec'))
|
||||
|
||||
@classmethod
|
||||
def fromname(cls, name):
|
||||
return cls.fromguess(name, guessit(name, {'type': 'episode'}))
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s [%r, %rx%r]>' % (self.__class__.__name__, self.series, self.season, self.episode)
|
||||
if self.year is None:
|
||||
return '<%s [%r, %dx%d]>' % (self.__class__.__name__, self.series, self.season, self.episode)
|
||||
|
||||
return '<%s [%r, %d, %dx%d]>' % (self.__class__.__name__, self.series, self.year, self.season, self.episode)
|
||||
|
||||
|
||||
class Movie(Video):
|
||||
"""Movie :class:`Video`
|
||||
"""Movie :class:`Video`.
|
||||
|
||||
Scores are defined by a set of equations, see :func:`~subliminal.score.get_movie_equations`
|
||||
|
||||
:param string title: title of the movie
|
||||
:param int year: year of the movie
|
||||
:param str title: title of the movie.
|
||||
:param int year: year of the movie.
|
||||
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
|
||||
|
||||
"""
|
||||
scores = {'title': 13, 'video_codec': 2, 'resolution': 2, 'audio_codec': 1, 'year': 7, 'imdb_id': 31,
|
||||
'release_group': 6, 'hash': 31}
|
||||
def __init__(self, name, title, year=None, **kwargs):
|
||||
super(Movie, self).__init__(name, **kwargs)
|
||||
|
||||
def __init__(self, name, title, release_group=None, resolution=None, video_codec=None, audio_codec=None,
|
||||
imdb_id=None, hashes=None, size=None, subtitle_languages=None, year=None):
|
||||
super(Movie, self).__init__(name, release_group, resolution, video_codec, audio_codec, imdb_id, hashes,
|
||||
size, subtitle_languages)
|
||||
#: Title of the movie
|
||||
self.title = title
|
||||
|
||||
#: Year of the movie
|
||||
self.year = year
|
||||
|
||||
@classmethod
|
||||
def fromguess(cls, name, guess):
|
||||
if guess['type'] != 'movie':
|
||||
raise ValueError('The guess must be a movie guess')
|
||||
|
||||
if 'title' not in guess:
|
||||
raise ValueError('Insufficient data to process the guess')
|
||||
return cls(name, guess['title'], release_group=guess.get('releaseGroup'), resolution=guess.get('screenSize'),
|
||||
video_codec=guess.get('videoCodec'), audio_codec=guess.get('audioCodec'),
|
||||
year=guess.get('year'))
|
||||
|
||||
return cls(name, guess['title'], format=guess.get('format'), release_group=guess.get('release_group'),
|
||||
resolution=guess.get('screen_size'), video_codec=guess.get('video_codec'),
|
||||
audio_codec=guess.get('audio_codec'), year=guess.get('year'))
|
||||
|
||||
@classmethod
|
||||
def fromname(cls, name):
|
||||
return cls.fromguess(name, guessit(name, {'type': 'movie'}))
|
||||
|
||||
def __repr__(self):
|
||||
if self.year is None:
|
||||
return '<%s [%r]>' % (self.__class__.__name__, self.title)
|
||||
return '<%s [%r, %r]>' % (self.__class__.__name__, self.title, self.year)
|
||||
|
||||
|
||||
def scan_subtitle_languages(path):
|
||||
"""Search for subtitles with alpha2 extension from a video `path` and return their language
|
||||
|
||||
:param string path: path to the video
|
||||
:return: found subtitle languages
|
||||
:rtype: set
|
||||
|
||||
"""
|
||||
language_extensions = tuple('.' + c for c in babelfish.get_language_converter('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:
|
||||
if st.language:
|
||||
try:
|
||||
embedded_subtitle_languages.add(babelfish.Language.fromalpha3b(st.language))
|
||||
except babelfish.Error:
|
||||
logger.error('Embedded subtitle track language %r is not a valid language', st.language)
|
||||
embedded_subtitle_languages.add(babelfish.Language('und'))
|
||||
elif st.name:
|
||||
try:
|
||||
embedded_subtitle_languages.add(babelfish.Language.fromname(st.name))
|
||||
except babelfish.Error:
|
||||
logger.error('Embedded subtitle track name %r is not a valid language', st.name)
|
||||
embedded_subtitle_languages.add(babelfish.Language('und'))
|
||||
else:
|
||||
embedded_subtitle_languages.add(babelfish.Language('und'))
|
||||
logger.debug('Found embedded subtitle %r with enzyme', embedded_subtitle_languages)
|
||||
video.subtitle_languages |= embedded_subtitle_languages
|
||||
else:
|
||||
logger.debug('MKV has no subtitle track')
|
||||
except enzyme.Error:
|
||||
logger.error('Parsing video metadata with enzyme failed')
|
||||
return video
|
||||
|
||||
|
||||
def scan_videos(paths, subtitles=True, embedded_subtitles=True, age=None):
|
||||
"""Scan `paths` for videos and their subtitle languages
|
||||
|
||||
:params paths: absolute paths to scan for videos
|
||||
:type paths: list of string
|
||||
:param bool subtitles: scan for subtitles with the same name
|
||||
:param bool embedded_subtitles: scan for embedded subtitles
|
||||
:param age: age of the video, if any
|
||||
:type age: datetime.timedelta or None
|
||||
:return: the scanned videos
|
||||
:rtype: list of :class:`Video`
|
||||
|
||||
"""
|
||||
videos = []
|
||||
# scan files
|
||||
for filepath in [p for p in paths if os.path.isfile(p)]:
|
||||
if age and datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(filepath)) > age:
|
||||
logger.info('Skipping video %r: older than %r', filepath, age)
|
||||
continue
|
||||
try:
|
||||
videos.append(scan_video(filepath, subtitles, embedded_subtitles))
|
||||
except ValueError as e:
|
||||
logger.error('Skipping video: %s', e)
|
||||
continue
|
||||
# scan directories
|
||||
for path in [p for p in paths if os.path.isdir(p)]:
|
||||
logger.info('Scanning directory %r', path)
|
||||
for dirpath, dirnames, filenames in os.walk(path):
|
||||
# skip badly encoded directories
|
||||
if isinstance(dirpath, bytes):
|
||||
logger.error('Skipping badly encoded directory %r', dirpath.decode('utf-8', errors='replace'))
|
||||
continue
|
||||
# skip badly encoded and hidden sub directories
|
||||
for dirname in list(dirnames):
|
||||
if isinstance(dirname, bytes):
|
||||
logger.error('Skipping badly encoded dirname %r in %r', dirname.decode('utf-8', errors='replace'),
|
||||
dirpath)
|
||||
dirnames.remove(dirname)
|
||||
elif dirname.startswith('.'):
|
||||
logger.debug('Skipping hidden dirname %r in %r', dirname, dirpath)
|
||||
dirnames.remove(dirname)
|
||||
# scan for videos
|
||||
for filename in filenames:
|
||||
# skip badly encoded files
|
||||
if isinstance(filename, bytes):
|
||||
logger.error('Skipping badly encoded filename %r in %r', filename.decode('utf-8', errors='replace'),
|
||||
dirpath)
|
||||
continue
|
||||
# filter videos
|
||||
if not filename.endswith(VIDEO_EXTENSIONS):
|
||||
continue
|
||||
# skip hidden files
|
||||
if filename.startswith('.'):
|
||||
logger.debug('Skipping hidden filename %r in %r', filename, dirpath)
|
||||
continue
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
# skip links
|
||||
if os.path.islink(filepath):
|
||||
logger.debug('Skipping link %r in %r', filename, dirpath)
|
||||
continue
|
||||
if age and datetime.datetime.now() - datetime.datetime.fromtimestamp(os.path.getmtime(filepath)) > age:
|
||||
logger.info('Skipping video %r: older than %r', filepath, age)
|
||||
continue
|
||||
try:
|
||||
video = scan_video(filepath, subtitles, embedded_subtitles)
|
||||
except ValueError as e:
|
||||
logger.error('Skipping video: %s', e)
|
||||
continue
|
||||
videos.append(video)
|
||||
return videos
|
||||
|
||||
|
||||
def hash_opensubtitles(video_path):
|
||||
"""Compute a hash using OpenSubtitles' algorithm
|
||||
|
||||
:param string video_path: path of the video
|
||||
:return: the hash
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
bytesize = struct.calcsize(b'q')
|
||||
with open(video_path, 'rb') as f:
|
||||
filesize = os.path.getsize(video_path)
|
||||
filehash = filesize
|
||||
if filesize < 65536 * 2:
|
||||
return None
|
||||
for _ in range(65536 / bytesize):
|
||||
filebuffer = f.read(bytesize)
|
||||
(l_value,) = struct.unpack(b'q', filebuffer)
|
||||
filehash += l_value
|
||||
filehash = filehash & 0xFFFFFFFFFFFFFFFF # to remain as 64bit number
|
||||
f.seek(max(0, filesize - 65536), 0)
|
||||
for _ in range(65536 / bytesize):
|
||||
filebuffer = f.read(bytesize)
|
||||
(l_value,) = struct.unpack(b'q', filebuffer)
|
||||
filehash += l_value
|
||||
filehash = filehash & 0xFFFFFFFFFFFFFFFF
|
||||
returnedhash = '%016x' % filehash
|
||||
return returnedhash
|
||||
|
||||
|
||||
def hash_thesubdb(video_path):
|
||||
"""Compute a hash using TheSubDB's algorithm
|
||||
|
||||
:param string video_path: path of the video
|
||||
:return: the hash
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
readsize = 64 * 1024
|
||||
if os.path.getsize(video_path) < readsize:
|
||||
return None
|
||||
with open(video_path, 'rb') as f:
|
||||
data = f.read(readsize)
|
||||
f.seek(-readsize, os.SEEK_END)
|
||||
data += f.read(readsize)
|
||||
return hashlib.md5(data).hexdigest().decode('ascii')
|
||||
return '<%s [%r, %d]>' % (self.__class__.__name__, self.title, self.year)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,240 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: username=subliminal&password=subliminal&Submit=Log+in
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
Content-Length: ['53']
|
||||
Content-Type: [application/x-www-form-urlencoded]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: POST
|
||||
uri: http://www.addic7ed.com/dologin.php
|
||||
response:
|
||||
body: {string: !!python/unicode "\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0\
|
||||
\ Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"\
|
||||
>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta http-equiv=\"\
|
||||
Content-Type\" content=\"text/html; charset=utf-8\" />\n<title>Addic7ed.com\
|
||||
\ - For all those TV Series Addic7s: Subtitles, Tv Series and Movies Talk,\
|
||||
\ Forum and more -</title>\n<link href=\"css/wikisubtitles.css\" rel=\"stylesheet\"\
|
||||
\ title=\"default\" type=\"text/css\" media=\"screen\"/>\n</head>\n\n<body>\n\
|
||||
<center><br />\n<table border=\"0\">\n<tr>\n <td rowspan=\"9\"><a href=\"\
|
||||
/\"><img height=\"200\" width=\"350\" src=\"http://www.addic7ed.com/images/addic7edheader.jpg\"\
|
||||
\ border=\"0\" title=\"Addic7ed.com - Quality Subtitles for TV Shows and\
|
||||
\ movies\" alt=\"Addic7ed.com - Quality Subtitles for TV Shows and movies\"\
|
||||
\ /></a></td>\n</tr>\n<tr><td align=\"center\" colspan=\"2\">\n<h1><small>Download\
|
||||
\ free subtitles for TV Shows and Movies.</small> \n<select name=\"\
|
||||
applang\" class=\"inputCool\" onchange=\"changeAppLang();\" id=\"comboLang\"\
|
||||
><option value=\"ar\">Arabic</option><option value=\"ca\">Catala</option><option\
|
||||
\ selected=\"selected\" value=\"en\">English</option><option value=\"eu\"\
|
||||
>Euskera</option><option value=\"fr\">French</option><option value=\"ga\"\
|
||||
>Galician</option><option value=\"de\">German</option><option value=\"gr\"\
|
||||
>Greek</option><option value=\"hu\">Hungarian</option><option value=\"it\"\
|
||||
>Italian</option><option value=\"fa\">Persian</option><option value=\"pl\"\
|
||||
>Polish</option><option value=\"pt\">Portuguese</option><option value=\"br\"\
|
||||
>Portuguese (Brazilian)</option><option value=\"ro\">Romanian</option><option\
|
||||
\ value=\"ru\">Russian</option><option value=\"es\">Spanish</option><option\
|
||||
\ value=\"se\">Swedish</option></select></h1>\n</td></tr>\n<tr><td align=\"\
|
||||
center\" colspan=\"2\">\n\n<script language=\"javascript\">\nvar url=\"/msgspopup.php?count=1\"\
|
||||
;\t\teditwin = window.open(url, \"msgswin\", 'height=200,width=350,toolbar=0,location=0,statusbar=0,menubar=0');\
|
||||
\ \n\t\tif (editwin.focus) {editwin.focus()}\n</script>\n<div id=\"hBar\"\
|
||||
>\n\t\t\t <ul>\n\t\t\t\t<li><a class=\"button white\" href=\"/panel.php\"\
|
||||
>My Panel</a></li><li><a class=\"button white\" href=\"/newsub.php\">Upload</a></li>\t\
|
||||
\t\t<li><a class=\"button white\" href=\"/shows.php\">Shows</a></li>\n\t\t\
|
||||
\t\t<li><a class=\"button white\" href=\"/allshows/a\">Air dates</a></li>\n\
|
||||
\t\t\t\t<li><a class=\"button white\" href=\"http://www.sub-talk.net\">Forum</a></li>\n\
|
||||
\t\t\t\t<li><a class=\"button white\" href=\"/logout.php\">Logout</a></li>\n\
|
||||
\t\t\t </ul>\n\t\t\t </div>\n</td></tr> \n<tr>\n <td>\n</td><td>\n\t<g:plusone\
|
||||
\ size=\"medium\"></g:plusone>\n <a href=\"http://twitter.com/addic7ed\"\
|
||||
\ target=\"_blank\"><img width=\"32\" height=\"32\" src=\"http://www.addic7ed.com/images/twitter_right.png\"\
|
||||
\ alt=\"Twitter\" border=\"0\" /></a>\n\t<a href=\"irc://irc.efnet.net:6667/addic7ed\"\
|
||||
><img width=\"32\" height=\"32\" src=\"http://www.addic7ed.com/images/irc-right.png\"\
|
||||
\ alt=\"IRC\" border=\"0\" /></a>\n<div style=\"float: right; padding-right:10%;\"\
|
||||
>\n\n </td>\n </tr>\n <tr>\n <td colspan=2><iframe src=\"http://www.facebook.com/plugins/like.php?href=https%3A%2F%2Fwww.facebook.com%2FAddic7ed&send=false&layout=button_count&width=450&show_faces=false&action=like&colorscheme=light&font=tahoma&height=21&appId=121322186712\"\
|
||||
\ scrolling=\"no\" frameborder=\"0\" style=\"border:none; overflow:hidden;\
|
||||
\ width:80px; height:21px;\" allowTransparency=\"true\"></iframe>\n </td>\n\
|
||||
\ </tr>\n</table>\n</center>\n\n<center>\n\n<!--[if lt IE 7]>\n <style type=\"\
|
||||
text/css\">\n div, img { behavior: url(http://www.addic7ed.com/js/iepngfix.htc)\
|
||||
\ }\n </style>\n<![endif]-->\n\n<center><table border=\"0\" width=\"90%\"\
|
||||
>\n<tr>\n<td class=\"NewsTitle\"><img width=\"20\" height=\"20\" src=\"http://www.addic7ed.com/images/television.png\"\
|
||||
\ alt=\"TV\" /><img src=\"http://www.addic7ed.com/images/invisible.gif\" alt=\"\
|
||||
\ \" />Addic7ed</td>\n<td class=\"NewsTitle\"><img width=\"20\" height=\"\
|
||||
20\" src=\"http://www.addic7ed.com/images/television.png\" alt=\"TV\" /><img\
|
||||
\ src=\"http://www.addic7ed.com/images/invisible.gif\" alt=\" \" />Popular\
|
||||
\ Shows</td>\n<td class=\"NewsTitle\"><img width=\"20\" height=\"20\" src=\"\
|
||||
http://www.addic7ed.com/images/television.png\" alt=\"TV\" /><img src=\"http://www.addic7ed.com/images/invisible.gif\"\
|
||||
\ alt=\" \" />Useful</td>\n<td class=\"NewsTitle\"><img width=\"20\" height=\"\
|
||||
20\" src=\"http://www.addic7ed.com/images/television.png\" alt=\"TV\" /><img\
|
||||
\ src=\"http://www.addic7ed.com/images/invisible.gif\" alt=\" \" />Forums</td>\n\
|
||||
</tr>\n<tr>\n<td><div id=\"footermenu\"><a href=\"/shows.php\">Browse By Shows</a></div></td>\n\
|
||||
<td><div id=\"footermenu\"><a href=\"/show/4906\">12 Monkeys</a></div></td>\n\
|
||||
<td><div id=\"footermenu\"><a href=\"/shows-schedule\">TV Shows Schedule</a></div></td>\n\
|
||||
<td><div id=\"footermenu\"><a href=\"http://www.sub-talk.net/topic/1031-changelog/\"\
|
||||
>Site Changelog</a></div></td>\n</tr>\n<tr>\n<td><div id=\"footermenu\"><a\
|
||||
\ href=\"/movie-subtitles\">Browse By Movies</a></div></td>\n<td><div id=\"\
|
||||
footermenu\"><a href=\"/show/1812\">Homeland</a></div></td>\n<td><div id=\"\
|
||||
footermenu\"><a href=\"http://www.sub-talk.net/topic/2784-frequently-asked-questions/\"\
|
||||
>Frequently Asked Questions</a></div></td>\n<td><div id=\"footermenu\">Support\
|
||||
\ Us</div></td>\n</tr>\n<tr>\n<td><div id=\"footermenu\"><a href=\"/top-uploaders\"\
|
||||
>Top Uploaders</a></div></td>\n<td><div id=\"footermenu\"><a href=\"/show/620\"\
|
||||
>Modern Family</a></div></td>\n<td><div id=\"footermenu\">RSS Feeds</div></td>\n\
|
||||
<td><div id=\"footermenu\">Premium Accounts</div></td>\n</tr>\n<tr>\n<td><div\
|
||||
\ id=\"footermenu\"><a href=\"/log.php?mode=downloaded\">Top Downloads</a></div></td>\n\
|
||||
<td><div id=\"footermenu\"><a href=\"/show/466\">Glee</a></div></td>\n<td\
|
||||
\ class=\"NewsTitle\"><img width=\"20\" height=\"20\" src=\"http://www.addic7ed.com/images/television.png\"\
|
||||
\ alt=\"TV\" /><img src=\"http://www.addic7ed.com/images/invisible.gif\" alt=\"\
|
||||
\ \"/>Tutorials</td>\n<td><div id=\"footermenu\"><a href=\"http://sub-talk.net/thread-6-1-1.html\"\
|
||||
>Video Formats</a></div></td>\n</tr>\n<tr>\n<td><div id=\"footermenu\"><a\
|
||||
\ href=\"/log.php?mode=news\">All News</a></div></td>\n<td><div id=\"footermenu\"\
|
||||
><a href=\"/show/450\">Parks and Recreation</a></div></td>\n<td><div id=\"\
|
||||
footermenu\"><a href=\"http://www.sub-talk.net/topic/338-guide-to-syncing-with-subtitleedit/page__p__1485__hl__%2B+%2Bsync__fromsearch__1#entry1485\"\
|
||||
>How to Synchronize Subtitles</a></div></td>\n<td><div id=\"footermenu\">Frequently\
|
||||
\ Asked Questions</div></td>\n</tr> \n<tr>\n<td><div id=\"footermenu\"><a\
|
||||
\ href=\"http://www.sub-talk.net\">Sub-Talk Forums</a></div></td>\n<td><div\
|
||||
\ id=\"footermenu\"><a href=\"/show/1277\">Shameless (US)</a></div></td>\n\
|
||||
<td><div id=\"footermenu\">What Are Subtitles</div></td>\n<td><div id=\"footermenu\"\
|
||||
><a href=\"http://sub-talk.net/index.php?gid=7\">TV Shows Talk</a></div></td>\n\
|
||||
</tr>\n<tr>\n<td><div id=\"footermenu\"><a href=\"/latest_comments.php\">Latest\
|
||||
\ Comments</a></div></td>\n<td><div id=\"footermenu\"><a href=\"/show/126\"\
|
||||
>The Big Bang Theory</a></div></td>\n<td><div id=\"footermenu\">New Translation\
|
||||
\ Tutorial</div></td>\n<td><div id=\"footermenu\"><a href=\"http://sub-talk.net/index.php?gid=22\"\
|
||||
>Movies Talk</a></div></td>\n</tr>\n<tr>\n<td><div id=\"footermenu\"><a href=\"\
|
||||
http://www.vreaubagaj.ro/troler/\" title=\"Trolere ieftine\" alt=\"Trolere\
|
||||
\ ieftine\">Trolere ieftine</a></div></td>\n<td><div id=\"footermenu\"><a\
|
||||
\ href=\"/show/130\">Family Guy</a></div></td>\n<td><div id=\"footermenu\"\
|
||||
>Upload a New Subtitle Tutorial</div></td>\n<td class=\"NewsTitle\"><img width=\"\
|
||||
20\" height=\"20\" src=\"http://www.addic7ed.com/images/television.png\" alt=\"\
|
||||
TV\" /><img src=\"http://www.addic7ed.com/images/invisible.gif\" alt=\" \"\
|
||||
\ />Stats</td>\n</tr>\n<tr>\n<td><div id=\"footermenu\">Terms of Service</div></td>\n\
|
||||
<td><div id=\"footermenu\"><a href=\"/show/1799\">American Horror Story</a></div></td>\n\
|
||||
<td><div id=\"footermenu\"><a href=\"http://sub-talk.net/viewthread.php?tid=294\"\
|
||||
>How to have an Avatar</a></div></td>\n<td align=\"left\">.\n\t\t\t\t</td>\n\
|
||||
</tr>\n<tr>\n<td><div id=\"footermenu\"><a href=\"/contact.php\">Contact</a></div></td>\n\
|
||||
<td><div id=\"footermenu\"><a href=\"/show/15\">House</a></div></td>\n<td><div\
|
||||
\ id=\"footermenu\"><a href=\"http://www.vreaubagaj.ro/\" alt=\"Trolere\"\
|
||||
\ title=\"Trolere\">Trolere</a></div></td>\n<td>\n</td>\n</tr>\n</table></center>\n\
|
||||
</center>\n\n<script type=\"text/javascript\">\nvar gaJsHost = ((\"https:\"\
|
||||
\ == document.location.protocol) ? \"https://ssl.\" : \"http://www.\");\n\
|
||||
document.write(unescape(\"%3Cscript src='\" + gaJsHost + \"google-analytics.com/ga.js'\
|
||||
\ type='text/javascript'%3E%3C/script%3E\"));\n</script>\n<script type=\"\
|
||||
text/javascript\">\ntry {\nvar pageTracker = _gat._getTracker(\"UA-10775680-1\"\
|
||||
);\npageTracker._trackPageview();\n} catch(err) {}</script>\n\n\n<script type=\"\
|
||||
text/javascript\" src=\"http://apis.google.com/js/plusone.js\"></script>\n\
|
||||
\ \
|
||||
\ \n</body>\n</html>\n"}
|
||||
headers:
|
||||
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
|
||||
connection: [keep-alive]
|
||||
content-type: [text/html]
|
||||
date: ['Tue, 09 Feb 2016 15:57:53 GMT']
|
||||
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
|
||||
location: [/]
|
||||
pragma: [no-cache]
|
||||
server: [nginx]
|
||||
set-cookie: [PHPSESSID=rk916i9m3r5gl5hmvs37l6m5p0; path=/]
|
||||
x-powered-by: [PHP/5.3.3]
|
||||
status: {code: 302, message: Moved Temporarily}
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
Cookie: [PHPSESSID=rk916i9m3r5gl5hmvs37l6m5p0]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: http://www.addic7ed.com/panel.php
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
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
|
||||
headers:
|
||||
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
|
||||
connection: [keep-alive]
|
||||
content-encoding: [gzip]
|
||||
content-type: [text/html]
|
||||
date: ['Tue, 09 Feb 2016 15:57:53 GMT']
|
||||
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
|
||||
pragma: [no-cache]
|
||||
server: [nginx]
|
||||
x-powered-by: [PHP/5.3.3]
|
||||
status: {code: 200, message: OK}
|
||||
version: 1
|
||||
@@ -0,0 +1,75 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: username=subliminal&password=lanimilbus&Submit=Log+in
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
Content-Length: ['53']
|
||||
Content-Type: [application/x-www-form-urlencoded]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: POST
|
||||
uri: http://www.addic7ed.com/dologin.php
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
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
|
||||
headers:
|
||||
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
|
||||
connection: [keep-alive]
|
||||
content-encoding: [gzip]
|
||||
content-type: [text/html]
|
||||
date: ['Tue, 09 Feb 2016 15:57:53 GMT']
|
||||
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
|
||||
pragma: [no-cache]
|
||||
server: [nginx]
|
||||
set-cookie: [PHPSESSID=4ep7cufhhkte78hs23ijqj12u4; path=/]
|
||||
x-powered-by: [PHP/5.3.3]
|
||||
status: {code: 200, message: OK}
|
||||
version: 1
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,228 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: http://www.addic7ed.com/search.php?search=The+Big+Bang+Theory&Submit=Search
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
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
|
||||
headers:
|
||||
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
|
||||
connection: [keep-alive]
|
||||
content-encoding: [gzip]
|
||||
content-type: [text/html]
|
||||
date: ['Tue, 09 Feb 2016 15:57:54 GMT']
|
||||
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
|
||||
pragma: [no-cache]
|
||||
server: [nginx]
|
||||
set-cookie: [PHPSESSID=vtudob3ke7abdfsgve42bidls4; path=/]
|
||||
x-powered-by: [PHP/5.3.3]
|
||||
status: {code: 200, message: OK}
|
||||
version: 1
|
||||
@@ -0,0 +1,97 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: http://www.addic7ed.com/search.php?search=The+Big+How+I+Met+Your+Mother&Submit=Search
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
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
|
||||
//Y3/gsAd1cq8jQAAA==
|
||||
headers:
|
||||
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
|
||||
connection: [keep-alive]
|
||||
content-encoding: [gzip]
|
||||
content-type: [text/html]
|
||||
date: ['Tue, 09 Feb 2016 15:57:58 GMT']
|
||||
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
|
||||
pragma: [no-cache]
|
||||
server: [nginx]
|
||||
set-cookie: [PHPSESSID=nrofdh6anqhelau1gujne69cs4; path=/]
|
||||
x-powered-by: [PHP/5.3.3]
|
||||
status: {code: 200, message: OK}
|
||||
version: 1
|
||||
@@ -0,0 +1,228 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: http://www.addic7ed.com/search.php?search=The+Big+Bang&Submit=Search
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
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
|
||||
headers:
|
||||
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
|
||||
connection: [keep-alive]
|
||||
content-encoding: [gzip]
|
||||
content-type: [text/html]
|
||||
date: ['Tue, 09 Feb 2016 15:57:55 GMT']
|
||||
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
|
||||
pragma: [no-cache]
|
||||
server: [nginx]
|
||||
set-cookie: [PHPSESSID=5t7377c0qtq8esk06s11u5qvf5; path=/]
|
||||
x-powered-by: [PHP/5.3.3]
|
||||
status: {code: 200, message: OK}
|
||||
version: 1
|
||||
@@ -0,0 +1,205 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: http://www.addic7ed.com/search.php?search=Dallas&Submit=Search
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
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
|
||||
headers:
|
||||
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
|
||||
connection: [keep-alive]
|
||||
content-encoding: [gzip]
|
||||
content-type: [text/html]
|
||||
date: ['Tue, 09 Feb 2016 15:57:56 GMT']
|
||||
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
|
||||
pragma: [no-cache]
|
||||
server: [nginx]
|
||||
set-cookie: [PHPSESSID=0o8itpncimkt6g8g151qo9ded7; path=/]
|
||||
x-powered-by: [PHP/5.3.3]
|
||||
status: {code: 200, message: OK}
|
||||
version: 1
|
||||
@@ -0,0 +1,236 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: http://www.addic7ed.com/search.php?search=Grey+s+Anatomy&Submit=Search
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
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
|
||||
headers:
|
||||
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
|
||||
connection: [keep-alive]
|
||||
content-encoding: [gzip]
|
||||
content-type: [text/html]
|
||||
date: ['Tue, 09 Feb 2016 15:57:58 GMT']
|
||||
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
|
||||
pragma: [no-cache]
|
||||
server: [nginx]
|
||||
set-cookie: [PHPSESSID=knqqbsvm08522036a1kfq9oi22; path=/]
|
||||
x-powered-by: [PHP/5.3.3]
|
||||
status: {code: 200, message: OK}
|
||||
version: 1
|
||||
@@ -0,0 +1,121 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate']
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Subliminal/2.0]
|
||||
method: GET
|
||||
uri: http://www.addic7ed.com/search.php?search=Dallas+2012&Submit=Search
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
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
|
||||
fpP1PWt3YKU8kBEHniH/7e79L7UagjE3VAAA
|
||||
headers:
|
||||
cache-control: ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0']
|
||||
connection: [keep-alive]
|
||||
content-encoding: [gzip]
|
||||
content-type: [text/html]
|
||||
date: ['Tue, 09 Feb 2016 15:57:57 GMT']
|
||||
expires: ['Thu, 19 Nov 1981 08:52:00 GMT']
|
||||
pragma: [no-cache]
|
||||
server: [nginx]
|
||||
set-cookie: [PHPSESSID=tdonebpl9qk72a0kr1fcarve56; path=/]
|
||||
x-powered-by: [PHP/5.3.3]
|
||||
status: {code: 200, message: OK}
|
||||
version: 1
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user